package cn.reghao.bnt.web.devops.app.service.impl;

import cn.reghao.bnt.common.docker.model.Config;
import cn.reghao.bnt.common.msg.constant.PackType;
import cn.reghao.bnt.common.msg.event.EvtAppStatResult;
import cn.reghao.bnt.web.admin.service.UserMessageService;
import cn.reghao.bnt.web.devops.aliyun.service.AliyunCdn;
import cn.reghao.bnt.web.devops.app.db.query.AppBuildQuery;
import cn.reghao.bnt.web.devops.app.db.query.AppDeployQuery;
import cn.reghao.bnt.web.devops.app.db.repository.AppDeployingRepository;
import cn.reghao.bnt.web.devops.app.db.repository.config.AppDeployConfigRepository;
import cn.reghao.bnt.web.devops.app.db.repository.log.DeployLogRepository;
import cn.reghao.bnt.web.devops.app.model.constant.DeployStatus;
import cn.reghao.bnt.web.devops.app.model.dto.DeployConfigDto;
import cn.reghao.bnt.web.devops.app.model.dto.DeployConfigUpdateDto;
import cn.reghao.bnt.web.devops.app.model.po.AppBuilding;
import cn.reghao.bnt.web.devops.app.model.po.AppDeploying;
import cn.reghao.bnt.web.devops.app.model.po.AppDeployingNode;
import cn.reghao.bnt.web.devops.app.model.po.config.AppConfig;
import cn.reghao.bnt.web.devops.app.model.po.config.AppDeployConfig;
import cn.reghao.bnt.web.devops.app.model.po.log.BuildLog;
import cn.reghao.bnt.web.devops.app.model.po.log.DeployLog;
import cn.reghao.bnt.web.devops.app.service.AppDeployService;
import cn.reghao.bnt.web.devops.app.service.bd.BuildDeployNotify;
import cn.reghao.bnt.web.devops.app.task.RereshCheckTask;
import cn.reghao.bnt.web.devops.machine.model.po.MachineHost;
import cn.reghao.bnt.web.devops.machine.service.MachineQuery;
import cn.reghao.jutil.jdk.converter.DateTimeConverter;
import cn.reghao.jutil.jdk.web.result.Result;
import cn.reghao.jutil.jdk.web.result.ResultStatus;
import cn.reghao.jutil.jdk.serializer.JsonConverter;
import cn.reghao.jutil.jdk.thread.ThreadPoolWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author reghao
 * @date 2021-10-22 14:45:22
 */
@Slf4j
@Service
public class AppDeployServiceImpl implements AppDeployService {
    private final AppDeployConfigRepository deployConfigRepository;
    private final AppDeployingRepository deployingRepository;
    private final DeployLogRepository deployLogRepository;
    private final BuildDeployNotify buildDeployNotify;
    private final MachineQuery machineQuery;
    private final AppBuildQuery appBuildQuery;
    private final AppDeployQuery appDeployQuery;
    private final UserMessageService userMessageService;
    private final AliyunCdn aliyunCdn;
    private final ScheduledExecutorService scheduler;

    public AppDeployServiceImpl(AppDeployConfigRepository deployConfigRepository, AppDeployingRepository deployingRepository,
                                DeployLogRepository deployLogRepository, BuildDeployNotify buildDeployNotify,
                                MachineQuery machineQuery, AppBuildQuery appBuildQuery,
                                AppDeployQuery appDeployQuery, UserMessageService userMessageService, AliyunCdn aliyunCdn) {
        this.deployConfigRepository = deployConfigRepository;
        this.deployingRepository = deployingRepository;
        this.deployLogRepository = deployLogRepository;
        this.buildDeployNotify = buildDeployNotify;
        this.machineQuery = machineQuery;
        this.appBuildQuery = appBuildQuery;
        this.appDeployQuery = appDeployQuery;
        this.userMessageService = userMessageService;
        this.aliyunCdn = aliyunCdn;
        this.scheduler = ThreadPoolWrapper.scheduledThreadPool("cdn-checker", 4);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public Result addDeployConfig(DeployConfigDto deployConfigDto) {
        String appId = deployConfigDto.getAppId();
        AppConfig appConfig = appBuildQuery.getAppConfig(appId);
        if (appConfig == null) {
            String msg = String.format("%s 不存在", appId);
            return Result.result(ResultStatus.ERROR, msg);
        }

        String machineId = deployConfigDto.getMachineId();
        MachineHost machineHost = machineQuery.getMachineHost(machineId);
        if (machineHost == null) {
            String msg = String.format("%s 不存在", machineId);
            return Result.result(ResultStatus.ERROR, msg);
        }

        String startScript = deployConfigDto.getStartScript();
        String packType = deployConfigDto.getPackType();
        if (packType.equals(PackType.docker.name())) {
            Config containerConfig = JsonConverter.jsonToObject(startScript, Config.class);
            if (startScript == null || startScript.isBlank()) {
                startScript = "{}";
            }
        }

        AppDeployConfig appDeployConfig = new AppDeployConfig(appConfig, machineHost, startScript);
        appDeployConfig = deployConfigRepository.save(appDeployConfig);

        AppDeploying appDeploying = deployingRepository.findByAppConfig_AppId(appId);
        if (appDeploying == null) {
            appDeploying = new AppDeploying(appDeployConfig);
        } else {
            AppDeployingNode appDeployingNode = new AppDeployingNode(appDeployConfig);
            appDeploying.getDeployingNodes().add(appDeployingNode);
        }
        deployingRepository.save(appDeploying);
        return Result.result(ResultStatus.SUCCESS);
    }

    @Override
    public Result updateStartScript(DeployConfigUpdateDto deployConfigUpdateDto) {
        String appId = deployConfigUpdateDto.getAppId();
        String machineId = deployConfigUpdateDto.getMachineId();
        AppDeployConfig appDeployConfig = appDeployQuery.getByAppIdAndMachineId(appId, machineId);
        if (appDeployConfig == null) {
            String msg = String.format("%s 在 %s 上的部署配置不存在", appId, machineId);
            return Result.result(ResultStatus.ERROR, msg);
        }

        String startScript = deployConfigUpdateDto.getStartScript();
        String packType = appDeployConfig.getAppConfig().getPackerConfig().getType();
        if (packType.equals(PackType.docker.name())) {
            Config containerConfig = JsonConverter.jsonToObject(startScript, Config.class);
            if (startScript == null || startScript.isBlank()) {
                startScript = "{}";
            }
        }

        appDeployConfig.setStartScript(startScript);
        deployConfigRepository.save(appDeployConfig);
        return Result.result(ResultStatus.SUCCESS);
    }

    @Override
    public void deleteDeployLog(String buildLogId) {
        deployLogRepository.deleteByBuildLog_BuildLogId(buildLogId);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public Result deleteDeployConfig(int appDeployConfigId) {
        AppDeployConfig appDeployConfig = deployConfigRepository.findById(appDeployConfigId).orElse(null);
        if (appDeployConfig == null) {
            return Result.result(ResultStatus.FAIL, "AppDeployConfig not exists");
        }

        return deleteAppDeployConfig(appDeployConfig);
    }

    private Result deleteAppDeployConfig(AppDeployConfig appDeployConfig) {
        String appId = appDeployConfig.getAppConfig().getAppId();
        String machineId = appDeployConfig.getMachineHost().getMachineId();
        if (!isRunning(appId, machineId)) {
            AppDeploying appDeploying = deployingRepository.findByAppConfig_AppId(appId);
            List<AppDeployingNode> deployingNodes = appDeploying.getDeployingNodes();
            int index = getIndex(deployingNodes, machineId);
            if (index != -1) {
                deployingNodes.remove(index);
            }

            if (deployingNodes.size() == 0) {
                deployingRepository.deleteByAppConfig_AppId(appId);
            } else {
                appDeploying.setDeployingNodes(deployingNodes);
                deployingRepository.save(appDeploying);
            }

            deployLogRepository.deleteByMachineHost_MachineId(machineId);
            deployConfigRepository.delete(appDeployConfig);
            return Result.result(ResultStatus.SUCCESS);
        }

        return Result.result(ResultStatus.FAIL, "app is running");
    }

    private boolean isRunning(String appId, String machineId) {
        AppDeploying appDeploying = deployingRepository.findByAppConfig_AppId(appId);
        if (appDeploying != null) {
            for (AppDeployingNode deployingNode : appDeploying.getDeployingNodes()) {
                String machineId1 = deployingNode.getDeployConfig().getMachineHost().getMachineId();
                if (machineId.equals(machineId1)) {
                    return deployingNode.getRunning();
                }
            }
        }

        return false;
    }

    @Override
    public Result deleteByApp(String appId) {
        List<AppDeployConfig> list = deployConfigRepository.findByAppConfig_AppId(appId);
        for (AppDeployConfig appDeployConfig : list) {
            Result result = deleteAppDeployConfig(appDeployConfig);
            if (result.getCode() != ResultStatus.SUCCESS.getCode()) {
                return result;
            }
        }

        return Result.result(ResultStatus.SUCCESS);
    }

    @Override
    public void deleteByMachineHost(MachineHost machineHost) {
        List<AppDeployConfig> list = deployConfigRepository.findByMachineHost(machineHost);
        List<String> apps = list.stream()
                .map(appDeployConfig -> appDeployConfig.getAppConfig().getAppId())
                .collect(Collectors.toList());

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < Math.min(3, list.size()); i++) {
            sb.append(apps.get(i)).append(",");
        }
        String msg = sb.toString();
        if (!msg.isBlank()) {
            String errMsg = String.format("还有 %s 等应用部署在当前机器上", msg.substring(0, msg.length()-1));
            log.error(errMsg);
        }

        for (AppDeployConfig appDeployConfig : list) {
            String appId = appDeployConfig.getAppConfig().getAppId();
            String machineId = appDeployConfig.getMachineHost().getMachineId();
            deleteAppDeploying(appId, machineId);
        }

        deployConfigRepository.deleteByMachineHost(machineHost);
    }

    @Override
    public void deleteAppDeploying(String appId, String machineId) {
        AppDeploying appDeploying = deployingRepository.findByAppConfig_AppId(appId);
        if (appDeploying != null) {
            List<AppDeployingNode> deployingNodes = appDeploying.getDeployingNodes();

            int idx = getIndex(deployingNodes, machineId);
            if (idx != -1) {
                deployingNodes.remove(idx);
                deployLogRepository.deleteByMachineHost_MachineId(machineId);
                deployingRepository.deleteByAppConfig_AppId(appId);
            }
        }
    }

    private int getIndex(List<AppDeployingNode> deployingNodes, String machineId) {
        int idx = -1;
        for (int i = 0; i < deployingNodes.size(); i++) {
            String machineId1 = deployingNodes.get(i).getDeployConfig().getMachineHost().getMachineId();
            if (machineId.equals(machineId1)) {
                idx = i;
                break;
            }
        }

        return idx;
    }

    @Override
    public void updateAfterBuild(AppBuilding appBuilding) {
        AppConfig appConfig = appBuilding.getAppConfig();
        AppDeploying appDeploying = deployingRepository.findByAppConfig(appConfig);
        if (appDeploying != null) {
            appDeploying.getDeployingNodes().forEach(deployNode -> {
                deployNode.setDeployResult(DeployStatus.neverDeploy.getDesc());
                deployNode.setOnDeploying(false);
                deployNode.setDeployed(false);
            });

            appDeploying.setBuildLog(appBuilding.getBuildLog());
            deployingRepository.save(appDeploying);
        }
    }

    @Override
    public void updateBeforeDeploy(AppBuilding appBuilding, String machineId, String deployBy) {
        AppDeploying appDeploying = deployingRepository.findByAppConfig(appBuilding.getAppConfig());
        appDeploying.setBuildLog(appBuilding.getBuildLog());
        appDeploying.getDeployingNodes().forEach(appDeployingNode -> {
            String machineId1 = appDeployingNode.getDeployConfig().getMachineHost().getMachineId();
            if (machineId1.equals(machineId)) {
                appDeployingNode.setDeployResult(DeployStatus.onDeploying.getDesc());
                appDeployingNode.setDeployBy(deployBy);
                appDeployingNode.setOnDeploying(true);
                appDeployingNode.setDeployed(false);
            }
        });

        deployingRepository.save(appDeploying);
    }

    @Override
    public void updateAfterDeploy(EvtAppStatResult deployResult) {
        log.info("执行 {} 部署后的操作", deployResult.getAppId());
        String machineId = deployResult.getMachineId();
        String appId = deployResult.getAppId();
        AppDeploying appDeploying = deployingRepository.findByAppConfig_AppId(appId);
        appDeploying.getDeployingNodes().forEach(deployNode -> {
            String machineId1 = deployNode.getDeployConfig().getMachineHost().getMachineId();
            if (machineId1.equals(machineId)) {
                deployNode.setOnDeploying(false);
                deployNode.setDeployed(true);
                deployNode.setDeployTime(DateTimeConverter.localDateTime(deployResult.getResult().getTimestamp()));
                if (deployResult.getResult().getCode() == ResultStatus.SUCCESS.getCode()) {
                    deployNode.setDeployResult(DeployStatus.deploySuccess.getDesc());
                    deployNode.setRunning(deployResult.getRunning());
                    deployNode.setStartTime(deployResult.getStartTime());
                    deployNode.setPid(deployResult.getPid());
                    deployNode.setLastCheck(DateTimeConverter.localDateTime(deployResult.getResult().getTimestamp()));
                } else {
                    deployNode.setDeployResult(DeployStatus.deployFail.getDesc());
                }

                MachineHost machineHost = deployNode.getDeployConfig().getMachineHost();
                BuildLog buildLog = appDeploying.getBuildLog();
                DeployLog deployLog = new DeployLog(buildLog, machineHost, deployResult);
                deployLogRepository.save(deployLog);

                buildDeployNotify.deployNotify(deployLog);
                log.info("{} 的通知已发送", deployResult.getAppId());
            }
        });

        deployingRepository.save(appDeploying);

        Result result = deployResult.getResult();
        if (result.getCode() == ResultStatus.SUCCESS.getCode()) {
            BuildLog buildLog = appDeploying.getBuildLog();
            AppConfig appConfig = appDeploying.getAppConfig();
            refreshCdn(appConfig, buildLog);
        }
    }

    private void refreshCdn(AppConfig appConfig, BuildLog buildLog) {
        String buildId = buildLog.getBuildLogId();
        String appId = appConfig.getAppId();
        String packType = appConfig.getPackerConfig().getType();
        if (packType.equals(PackType.staticFiles.getName())) {
            String domains = appConfig.getDomains();
            if (domains == null) {
                return;
            }

            String[] domainArr = StringUtils.trimAllWhitespace(domains).split(",");
            for (String domain : domainArr) {
                try {
                    String taskId = aliyunCdn.refreshCdn(domain);
                    if (taskId != null) {
                        log.info("taskId -> {}", taskId);
                        RereshCheckTask rereshCheckTask =
                                new RereshCheckTask(aliyunCdn, buildDeployNotify, taskId, buildId, appId, domain);
                        scheduler.schedule(rereshCheckTask, 20, TimeUnit.SECONDS);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void updateAppDeployingNode(EvtAppStatResult statResult) {
        String appId = statResult.getAppId();
        String machineId = statResult.getMachineId();
        AppDeploying appDeploying = deployingRepository.findByAppConfig_AppId(appId);
        if (appDeploying != null) {
            appDeploying.getDeployingNodes().forEach(appDeployingNode -> {
                String machineId1 = appDeployingNode.getDeployConfig().getMachineHost().getMachineId();
                if (machineId1.equals(machineId)) {
                    appDeployingNode.setRunning(statResult.getRunning());
                    appDeployingNode.setStartTime(statResult.getStartTime());
                    appDeployingNode.setPid(statResult.getPid());
                    if (statResult.getResult() != null) {
                        appDeployingNode.setLastCheck(DateTimeConverter.localDateTime(statResult.getResult().getTimestamp()));
                    }
                }
            });

            deployingRepository.save(appDeploying);
        } else {
            String machineIpv4 = "0.0.0.0";
            MachineHost machineHost = machineQuery.getMachineHost(machineId);
            if (machineHost != null) {
                machineIpv4 = machineHost.getMachineIpv4();
            }

            String title = "App Not Exist";
            String content = String.format("%s not exist on node %s", appId, machineIpv4);
            userMessageService.sendUserMessage(title, content);
        }
    }
}
