/*
 *  Copyright 2019-2020 Zheng Jie
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package me.zhengjie.modules.mnt.service.impl;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.modules.mnt.domain.App;
import me.zhengjie.modules.mnt.domain.Deploy;
import me.zhengjie.modules.mnt.domain.DeployHistory;
import me.zhengjie.modules.mnt.domain.ServerDeploy;
import me.zhengjie.modules.mnt.repository.DeployRepository;
import me.zhengjie.modules.mnt.service.DeployHistoryService;
import me.zhengjie.modules.mnt.service.DeployService;
import me.zhengjie.modules.mnt.service.ServerDeployService;
import me.zhengjie.modules.mnt.service.dto.AppDto;
import me.zhengjie.modules.mnt.service.dto.DeployDto;
import me.zhengjie.modules.mnt.service.dto.DeployQueryCriteria;
import me.zhengjie.modules.mnt.service.dto.ServerDeployDto;
import me.zhengjie.modules.mnt.service.mapstruct.DeployMapper;
import me.zhengjie.modules.mnt.util.ExecuteShellUtil;
import me.zhengjie.modules.mnt.util.ScpClientUtil;
import me.zhengjie.modules.mnt.websocket.MsgType;
import me.zhengjie.modules.mnt.websocket.SocketMsg;
import me.zhengjie.modules.mnt.websocket.WebSocketServer;
import me.zhengjie.utils.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * @author zhanghouying
 * @date 2019-08-24
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DeployServiceImpl implements DeployService {

	private final String FILE_SEPARATOR = "/";
	private final DeployRepository deployRepository;
	private final DeployMapper deployMapper;
	private final ServerDeployService serverDeployService;
	private final DeployHistoryService deployHistoryService;
	/**
	 * 循环次数
	 */
	private final Integer count = 30;


	@Override
	public Object queryAll(DeployQueryCriteria criteria, Pageable pageable) {
		Page<Deploy> page = deployRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder), pageable);
		return PageUtil.toPage(page.map(deployMapper::toDto));
	}

	@Override
	public List<DeployDto> queryAll(DeployQueryCriteria criteria) {
		return deployMapper.toDto(deployRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder)));
	}

	@Override
	public DeployDto findById(Long id) {
		Deploy deploy = deployRepository.findById(id).orElseGet(Deploy::new);
		ValidationUtil.isNull(deploy.getId(), "Deploy", "id", id);
		return deployMapper.toDto(deploy);
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public void create(Deploy resources) {
		deployRepository.save(resources);
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public void update(Deploy resources) {
		Deploy deploy = deployRepository.findById(resources.getId()).orElseGet(Deploy::new);
		ValidationUtil.isNull(deploy.getId(), "Deploy", "id", resources.getId());
		deploy.copy(resources);
		deployRepository.save(deploy);
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public void delete(Set<Long> ids) {
		for (Long id : ids) {
			deployRepository.deleteById(id);
		}
	}

	@Override
	public void deploy(String fileSavePath, Long id) {
		deployApp(fileSavePath, id);
	}

	/**
	 * @param fileSavePath 本机路径
	 * @param id ID
	 */
	private void deployApp(String fileSavePath, Long id) {

		DeployDto deploy = findById(id);
		if (deploy == null) {
			sendMsg("部署信息不存在", MsgType.ERROR);
			throw new BadRequestException("部署信息不存在");
		}
		AppDto app = deploy.getApp();
		if (app == null) {
			sendMsg("包对应应用信息不存在", MsgType.ERROR);
			throw new BadRequestException("包对应应用信息不存在");
		}
		int port = app.getPort();
		//这个是服务器部署路径
		String uploadPath = app.getUploadPath();
		StringBuilder sb = new StringBuilder();
		String msg;
		Set<ServerDeployDto> deploys = deploy.getDeploys();
		for (ServerDeployDto deployDTO : deploys) {
			String ip = deployDTO.getIp();
			ExecuteShellUtil executeShellUtil = getExecuteShellUtil(ip);
			//判断是否第一次部署
			boolean flag = checkFile(executeShellUtil, app);
			//第一步要确认服务器上有这个目录
			executeShellUtil.execute("mkdir -p " + app.getUploadPath());
			executeShellUtil.execute("mkdir -p " + app.getBackupPath());
			executeShellUtil.execute("mkdir -p " + app.getDeployPath());
			//上传文件
			msg = String.format("登陆到服务器:%s", ip);
			ScpClientUtil scpClientUtil = getScpClientUtil(ip);
			log.info(msg);
			sendMsg(msg, MsgType.INFO);
			msg = String.format("上传文件到服务器:%s<br>目录:%s下，请稍等...", ip, uploadPath);
			sendMsg(msg, MsgType.INFO);
			scpClientUtil.putFile(fileSavePath, uploadPath);
			if (flag) {
				sendMsg("停止原来应用", MsgType.INFO);
				//停止应用
				stopApp(port, executeShellUtil);
				sendMsg("备份原来应用", MsgType.INFO);
				//备份应用
				backupApp(executeShellUtil, ip, app.getDeployPath()+FILE_SEPARATOR, app.getName(), app.getBackupPath()+FILE_SEPARATOR, id);
			}
			sendMsg("部署应用", MsgType.INFO);
			//部署文件,并启动应用
			String deployScript = app.getDeployScript();
			executeShellUtil.execute(deployScript);
			sleep(3);
			sendMsg("应用部署中，请耐心等待部署结果，或者稍后手动查看部署状态", MsgType.INFO);
			int i  = 0;
			boolean result = false;
			// 由于启动应用需要时间，所以需要循环获取状态，如果超过30次，则认为是启动失败
			while (i++ < count){
				result = checkIsRunningStatus(port, executeShellUtil);
				if(result){
					break;
				}
				// 休眠6秒
				sleep(6);
			}
			sb.append("服务器:").append(deployDTO.getName()).append("<br>应用:").append(app.getName());
			sendResultMsg(result, sb);
			executeShellUtil.close();
		}
	}

	private void sleep(int second) {
		try {
			Thread.sleep(second * 1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private void backupApp(ExecuteShellUtil executeShellUtil, String ip, String fileSavePath, String appName, String backupPath, Long id) {
		String deployDate = DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN);
		StringBuilder sb = new StringBuilder();
		backupPath += appName + FILE_SEPARATOR + deployDate + "\n";
		sb.append("mkdir -p ").append(backupPath);
		sb.append("mv -f ").append(fileSavePath);
		sb.append(appName).append(" ").append(backupPath);
		log.info("备份应用脚本:" + sb.toString());
		executeShellUtil.execute(sb.toString());
		//还原信息入库
		DeployHistory deployHistory = new DeployHistory();
		deployHistory.setAppName(appName);
		deployHistory.setDeployUser(SecurityUtils.getCurrentUsername());
		deployHistory.setIp(ip);
		deployHistory.setDeployId(id);
		deployHistoryService.create(deployHistory);
	}

	/**
	 * 停App
	 *
	 * @param port 端口
	 * @param executeShellUtil /
	 */
	private void stopApp(int port, ExecuteShellUtil executeShellUtil) {
		//发送停止命令
		executeShellUtil.execute(String.format("lsof -i :%d|grep -v \"PID\"|awk '{print \"kill -9\",$2}'|sh", port));

	}

	/**
	 * 指定端口程序是否在运行
	 *
	 * @param port 端口
	 * @param executeShellUtil /
	 * @return true 正在运行  false 已经停止
	 */
	private boolean checkIsRunningStatus(int port, ExecuteShellUtil executeShellUtil) {
		String result = executeShellUtil.executeForResult(String.format("fuser -n tcp %d", port));
		return result.indexOf("/tcp:")>0;
	}

	private void sendMsg(String msg, MsgType msgType) {
		try {
			WebSocketServer.sendInfo(new SocketMsg(msg, msgType), "deploy");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public String serverStatus(Deploy resources) {
		Set<ServerDeploy> serverDeploys = resources.getDeploys();
		App app = resources.getApp();
		for (ServerDeploy serverDeploy : serverDeploys) {
			StringBuilder sb = new StringBuilder();
			ExecuteShellUtil executeShellUtil = getExecuteShellUtil(serverDeploy.getIp());
			sb.append("服务器:").append(serverDeploy.getName()).append("<br>应用:").append(app.getName());
			boolean result = checkIsRunningStatus(app.getPort(), executeShellUtil);
			if (result) {
				sb.append("<br>正在运行");
				sendMsg(sb.toString(), MsgType.INFO);
			} else {
				sb.append("<br>已停止!");
				sendMsg(sb.toString(), MsgType.ERROR);
			}
			log.info(sb.toString());
			executeShellUtil.close();
		}
		return "执行完毕";
	}

	private boolean checkFile(ExecuteShellUtil executeShellUtil, AppDto appDTO) {
		String result = executeShellUtil.executeForResult("find " + appDTO.getDeployPath() + " -name " + appDTO.getName());
		return result.indexOf(appDTO.getName())>0;
	}

	/**
	 * 启动服务
	 * @param resources /
	 * @return /
	 */
	@Override
	public String startServer(Deploy resources) {
		Set<ServerDeploy> deploys = resources.getDeploys();
		App app = resources.getApp();
		for (ServerDeploy deploy : deploys) {
			StringBuilder sb = new StringBuilder();
			ExecuteShellUtil executeShellUtil = getExecuteShellUtil(deploy.getIp());
			//为了防止重复启动，这里先停止应用
			stopApp(app.getPort(), executeShellUtil);
			sb.append("服务器:").append(deploy.getName()).append("<br>应用:").append(app.getName());
			sendMsg("下发启动命令", MsgType.INFO);
			executeShellUtil.execute(app.getStartScript());
			sleep(3);
			sendMsg("应用启动中，请耐心等待启动结果，或者稍后手动查看运行状态", MsgType.INFO);
			int i  = 0;
			boolean result = false;
			// 由于启动应用需要时间，所以需要循环获取状态，如果超过30次，则认为是启动失败
			while (i++ < count){
				result = checkIsRunningStatus(app.getPort(), executeShellUtil);
				if(result){
					break;
				}
				// 休眠6秒
				sleep(6);
			}
			sendResultMsg(result, sb);
			log.info(sb.toString());
			executeShellUtil.close();
		}
		return "执行完毕";
	}

	/**
	 * 停止服务
	 * @param resources /
	 * @return /
	 */
	@Override
	public String stopServer(Deploy resources) {
		Set<ServerDeploy> deploys = resources.getDeploys();
		App app = resources.getApp();
		for (ServerDeploy deploy : deploys) {
			StringBuilder sb = new StringBuilder();
			ExecuteShellUtil executeShellUtil = getExecuteShellUtil(deploy.getIp());
			sb.append("服务器:").append(deploy.getName()).append("<br>应用:").append(app.getName());
			sendMsg("下发停止命令", MsgType.INFO);
			//停止应用
			stopApp(app.getPort(), executeShellUtil);
			sleep(1);
			boolean result = checkIsRunningStatus(app.getPort(), executeShellUtil);
			if (result) {
				sb.append("<br>关闭失败!");
				sendMsg(sb.toString(), MsgType.ERROR);
			} else {
				sb.append("<br>关闭成功!");
				sendMsg(sb.toString(), MsgType.INFO);
			}
			log.info(sb.toString());
			executeShellUtil.close();
		}
		return "执行完毕";
	}

	@Override
	public String serverReduction(DeployHistory resources) {
		Long deployId = resources.getDeployId();
		Deploy deployInfo = deployRepository.findById(deployId).orElseGet(Deploy::new);
		String deployDate = DateUtil.format(resources.getDeployDate(), DatePattern.PURE_DATETIME_PATTERN);
		App app = deployInfo.getApp();
		if (app == null) {
			sendMsg("应用信息不存在：" + resources.getAppName(), MsgType.ERROR);
			throw new BadRequestException("应用信息不存在：" + resources.getAppName());
		}
		String backupPath = app.getBackupPath()+FILE_SEPARATOR;
		backupPath += resources.getAppName() + FILE_SEPARATOR + deployDate;
		//这个是服务器部署路径
		String deployPath = app.getDeployPath();
		String ip = resources.getIp();
		ExecuteShellUtil executeShellUtil = getExecuteShellUtil(ip);
		String msg;

		msg = String.format("登陆到服务器:%s", ip);
		log.info(msg);
		sendMsg(msg, MsgType.INFO);
		sendMsg("停止原来应用", MsgType.INFO);
		//停止应用
		stopApp(app.getPort(), executeShellUtil);
		//删除原来应用
		sendMsg("删除应用", MsgType.INFO);
		executeShellUtil.execute("rm -rf " + deployPath + FILE_SEPARATOR + resources.getAppName());
		//还原应用
		sendMsg("还原应用", MsgType.INFO);
		executeShellUtil.execute("cp -r " + backupPath + "/. " + deployPath);
		sendMsg("启动应用", MsgType.INFO);
		executeShellUtil.execute(app.getStartScript());
		sendMsg("应用启动中，请耐心等待启动结果，或者稍后手动查看启动状态", MsgType.INFO);
		int i  = 0;
		boolean result = false;
		// 由于启动应用需要时间，所以需要循环获取状态，如果超过30次，则认为是启动失败
		while (i++ < count){
			result = checkIsRunningStatus(app.getPort(), executeShellUtil);
			if(result){
				break;
			}
			// 休眠6秒
			sleep(6);
		}
		StringBuilder sb = new StringBuilder();
		sb.append("服务器:").append(ip).append("<br>应用:").append(resources.getAppName());
		sendResultMsg(result, sb);
		executeShellUtil.close();
		return "";
	}

	private ExecuteShellUtil getExecuteShellUtil(String ip) {
		ServerDeployDto serverDeployDTO = serverDeployService.findByIp(ip);
		if (serverDeployDTO == null) {
			sendMsg("IP对应服务器信息不存在：" + ip, MsgType.ERROR);
			throw new BadRequestException("IP对应服务器信息不存在：" + ip);
		}
		return new ExecuteShellUtil(ip, serverDeployDTO.getAccount(), serverDeployDTO.getPassword(),serverDeployDTO.getPort());
	}

	private ScpClientUtil getScpClientUtil(String ip) {
		ServerDeployDto serverDeployDTO = serverDeployService.findByIp(ip);
		if (serverDeployDTO == null) {
			sendMsg("IP对应服务器信息不存在：" + ip, MsgType.ERROR);
			throw new BadRequestException("IP对应服务器信息不存在：" + ip);
		}
		return ScpClientUtil.getInstance(ip, serverDeployDTO.getPort(), serverDeployDTO.getAccount(), serverDeployDTO.getPassword());
	}

	private void sendResultMsg(boolean result, StringBuilder sb) {
		if (result) {
			sb.append("<br>启动成功!");
			sendMsg(sb.toString(), MsgType.INFO);
		} else {
			sb.append("<br>启动失败!");
			sendMsg(sb.toString(), MsgType.ERROR);
		}
	}

	@Override
	public void download(List<DeployDto> queryAll, HttpServletResponse response) throws IOException {
		List<Map<String, Object>> list = new ArrayList<>();
		for (DeployDto deployDto : queryAll) {
			Map<String,Object> map = new LinkedHashMap<>();
			map.put("应用名称", deployDto.getApp().getName());
			map.put("服务器", deployDto.getServers());
			map.put("部署日期", deployDto.getCreateTime());
			list.add(map);
		}
		FileUtil.downloadExcel(list, response);
	}
}
