package com.lanyuanxiaoyao.service.cli.runner;

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.IoUtil;
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.RuntimeInfo;
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.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.factory.Maps;
import org.eclipse.collections.api.list.ImmutableList;
import org.eclipse.collections.api.list.MutableList;
import org.eclipse.collections.api.map.MutableMap;
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;
import org.springframework.core.io.ClassPathResource;

/**
 * 启动类
 *
 * @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 RuntimeInfo runtimeInfo;
    private final ImmutableList<ServiceInfoWrapper> serviceInfoList;
    private final ImmutableList<HostInfoWrapper> hostInfoList;
    private final ObjectMapper mapper;

    public RunnerApplication(DeployInformationProperties deployInformationProperties) {
        this.deployInformationProperties = deployInformationProperties;
        runtimeInfo = deployInformationProperties.getRuntime();
        serviceInfoList = Lists.immutable.ofAll(deployInformationProperties.getServices().entrySet())
                .collect(entry -> new ServiceInfoWrapper(entry.getKey(), entry.getValue()))
                .select(ServiceInfoWrapper::getEnabled)
                .toSortedListBy(ServiceInfoWrapper::getOrder)
                .toImmutable();
        hostInfoList = Lists.immutable.ofAll(SpringPropertiesEscapeHelper.escapeMapKey(deployInformationProperties.getHosts()).entrySet())
                .collect(entry -> new HostInfoWrapper(entry.getKey(), entry.getValue()))
                .toSortedListBy(HostInfoWrapper::getHost)
                .toImmutable();
        mapper = new ObjectMapper();
    }

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

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

    @Override
    public void run(ApplicationArguments args) throws IOException {
        logger.info("Loading runtime info: {}", mapper.writeValueAsString(runtimeInfo));

        if (deployInformationProperties.getGenerate().getCloud())
            generateCloud(Paths.get("cloud"));
        if (deployInformationProperties.getGenerate().getCommand())
            generateCommand(Paths.get("command"));
    }

    private void generateCloud(Path root) throws IOException {
        Files.createDirectories(root);
        String absolutRootPath = root.toAbsolutePath().toString();
        logger.info("Current path: {}", absolutRootPath);

        Path planPath = Paths.get(root.toString(), "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>>>() {});
        }

        ImmutableList<String> clusters = Lists.mutable.of(runtimeInfo.getYarn().getSyncClusters().split(","))
                .withAll(Lists.mutable.of(runtimeInfo.getYarn().getCompactionClusters().split(",")))
                .collect(n -> runtimeInfo.getYarn().getStoreCluster() + n)
                .distinct()
                .toImmutable();

        TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
        Template deployTemplate = engine.getTemplate("cloud/deploy.ftl");
        for (ServiceInfoWrapper serviceInfo : serviceInfoList) {
            logger.info("Generate script for {}", serviceInfo.getName());
            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("currentPath", absolutRootPath)
                    .put("hosts", hostInfoList
                            .collect(HostInfoWrapper::getHostnameIp)
                            .toSortedList((o1, o2) -> Comparator.<String>naturalOrder().compare(o1.getIp(), o2.getIp())))
                    .put("selectedHosts", selectedHosts)
                    .put("runtime", runtimeInfo)
                    .put("info", serviceInfo)
                    .put("classpath", String.join(":", serviceInfo.getClasspath()))
                    .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("cloud/stop.ftl");
            String stopScript = stopTemplate.render(MapUtil.builder()
                    .put("currentPath", absolutRootPath)
                    .put("hosts", hostInfoList
                            .collect(HostInfoWrapper::getIp)
                            .toSortedList(Comparator.naturalOrder()))
                    .put("runtime", runtimeInfo)
                    .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("cloud/log.ftl");
            String logScript = logTemplate.render(MapUtil.builder()
                    .put("currentPath", absolutRootPath)
                    .put("hosts", hostInfoList
                            .collect(HostInfoWrapper::getIp)
                            .toSortedList(Comparator.naturalOrder()))
                    .put("selectedHosts", selectedHosts)
                    .put("runtime", runtimeInfo)
                    .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());
        }

        ClassPathResource stopFile = new ClassPathResource("script/stop.sh");
        String stopScript = new String(IoUtil.readBytes(stopFile.getInputStream()));
        Files.write(Paths.get(root.toString(), "stop.sh"), stopScript.getBytes());

        MutableMap<String, MutableList<ServiceInfoWrapper>> groups = Maps.mutable.empty();
        for (ServiceInfoWrapper service : serviceInfoList) {
            service.getGroups().add(0, "all");
            for (String group : service.getGroups()) {
                if (!groups.containsKey(group)) {
                    groups.put(group, Lists.mutable.empty());
                }
                List<ServiceInfoWrapper> list = groups.get(group);
                list.add(service);
            }
        }
        for (Map.Entry<String, MutableList<ServiceInfoWrapper>> entry : groups.entrySet()) {
            String group = entry.getKey();
            MutableList<ServiceInfoWrapper> infos = entry.getValue();

            Template batchDeployTemplate = engine.getTemplate("cloud/batch-deploy.ftl");
            String batchDeployScript = batchDeployTemplate.render(MapUtil.builder()
                    .put("currentPath", absolutRootPath)
                    .put("services", infos.collect(ServiceInfoWrapper::getName))
                    .build());
            Path batchDeployScriptFile = Paths.get(
                    root.toString(),
                    StrUtil.format("batch-deploy-{}.sh", group)
            );
            Files.deleteIfExists(batchDeployScriptFile);
            Files.write(batchDeployScriptFile, batchDeployScript.getBytes());

            Template batchStopTemplate = engine.getTemplate("cloud/batch-stop.ftl");
            String batchStopScript = batchStopTemplate.render(MapUtil.builder()
                    .put("currentPath", absolutRootPath)
                    .put("services", infos.collect(ServiceInfoWrapper::getName))
                    .build());
            Path batchStopScriptFile = Paths.get(
                    root.toString(),
                    StrUtil.format("batch-stop-{}.sh", group)
            );
            Files.deleteIfExists(batchStopScriptFile);
            Files.write(batchStopScriptFile, batchStopScript.getBytes());
        }

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

    private void generateCommand(Path root) throws IOException {
        Files.createDirectories(root);
        String absolutRootPath = root.toAbsolutePath().toString();
        logger.info("Current path: {}", absolutRootPath);

        TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
        Template commandTemplate = engine.getTemplate("command/cli.ftl");
        String commandScript = commandTemplate.render(MapUtil.builder()
                .put("currentPath", absolutRootPath)
                .put("runtime", runtimeInfo)
                .put("directly", false)
                .build());
        Path commandScriptFile = Paths.get(root.toString(), "cli");
        Files.deleteIfExists(commandScriptFile);
        Files.write(commandScriptFile, commandScript.getBytes());

        Template commandDirectlyTemplate = engine.getTemplate("command/cli.ftl");
        String commandDirectlyScript = commandDirectlyTemplate.render(MapUtil.builder()
                .put("currentPath", absolutRootPath)
                .put("runtime", runtimeInfo)
                .put("directly", true)
                .build());
        Path commandDirectlyScriptFile = Paths.get(root.toString(), "cli_d");
        Files.deleteIfExists(commandDirectlyScriptFile);
        Files.write(commandDirectlyScriptFile, commandDirectlyScript.getBytes());

        Template updateTemplate = engine.getTemplate("command/update.ftl");
        String updateScript = updateTemplate.render(MapUtil.builder()
                .put("currentPath", absolutRootPath)
                .put("runtime", runtimeInfo)
                .build());
        Path updateScriptFile = Paths.get(root.toString(), "update.sh");
        Files.deleteIfExists(updateScriptFile);
        Files.write(updateScriptFile, updateScript.getBytes());
    }
}
