package com.fdkankan.modeling.receiver;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.fdkankan.common.constant.ConstantFilePath;
import com.fdkankan.common.constant.ModelingBuildStatus;
import com.fdkankan.common.constant.ServerCode;
import com.fdkankan.common.constant.UploadFilePath;
import com.fdkankan.common.exception.BusinessException;
import com.fdkankan.common.util.ComputerUtil;
import com.fdkankan.common.util.CreateObjUtil;
import com.fdkankan.common.util.FileUtils;
import com.fdkankan.common.util.SceneUtil;
import com.fdkankan.dingtalk.DingTalkSendUtils;
import com.fdkankan.fyun.oss.UploadToOssUtil;
import com.fdkankan.modeling.bean.BuildSceneResultBean;
import com.fdkankan.modeling.constants.SysConstants;
import com.fdkankan.modeling.entity.BuildLog;
import com.fdkankan.modeling.exception.BuildException;
import com.fdkankan.modeling.service.IBuildLogService;
import com.fdkankan.rabbitmq.bean.BuildSceneCallMessage;
import com.fdkankan.rabbitmq.bean.BuildSceneFailDTMqMessage;
import com.fdkankan.rabbitmq.bean.BuildSceneResultMqMessage;
import com.fdkankan.rabbitmq.util.RabbitMqProducer;
import com.fdkankan.redis.constant.RedisKey;
import com.fdkankan.redis.util.RedisUtil;
import com.rabbitmq.client.Channel;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.client.RestTemplate;

/**
 * <p>
 * TODO
 * </p>
 *
 * @author dengsixing
 * @since 2022/4/20
 **/
@Slf4j
@Component
public class RabbitMqListener {

    @Value("${queue.modeling.modeling-call}")
    private String queueModelingCall;
    @Value("${queue.modeling.modeling-dt}")
    private String queueModelingDt;
    @Value("${queue.modeling.modeling-post}")
    private String queueModelingPost;

    @Autowired
    RedisUtil redisUtil;

    private RestTemplate restTemplate = new RestTemplate();

    @Value("${4dkk.laserService.host}")
    private String laserHost;

    @Value("${upload.type}")
    private String ossType;

    @Value("${oss.prefix.ali}")
    private String prefixAli;

    @Autowired
    private UploadToOssUtil uploadToOssUtil;

    @Autowired
    RabbitMqProducer rabbitMqProducer;

    @Value("${spring.profiles.active}")
    private String environment;

    @Autowired
    private DingTalkSendUtils dingTalkSendUtils;

    @Autowired
    private IBuildLogService buildLogService;

    /**
     * 场景计算
     * @param channel
     * @param message
     * @throws Exception
     */
    @RabbitListener(
        queuesToDeclare = @Queue("${queue.modeling.modeling-call}"),
        concurrency = "${maxThread.modeling.modeling-call}"
    )
    public void buildSceneHandler(Channel channel, Message message) throws Exception {
        Object correlation = message.getMessageProperties().getHeader("spring_returned_message_correlation");
        String correlationId = (String) correlation;
        if (ObjectUtils.isEmpty(message.getBody())) {
            SysConstants.SYSTEM_BUILDING = false;
            log.error("消息体为空,退出构建,当前服务器:{}", SysConstants.hostName);
            return;
        }
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        BuildSceneCallMessage buildSceneMessage = JSONObject.parseObject(msg, BuildSceneCallMessage.class);
        log.info("场景计算开始,队列名:{},id:{},消息体:{}", queueModelingCall, correlationId,msg);
        if(Objects.isNull(buildSceneMessage) || StrUtil.isBlank(buildSceneMessage.getSceneNum())){
            log.error("消息内容错误,id:{},消息体:{}", correlationId, msg);
            return;
        }

        Thread.sleep(2000L);
        this.process(buildSceneMessage);

        log.info("场景计算开始,队列名:{},id:{}", queueModelingCall, correlationId);

        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

    public void process(BuildSceneCallMessage message) {

        SysConstants.SYSTEM_BUILDING = true;
        if (SysConstants.SYSTEM_OFFING) {
            SysConstants.SYSTEM_BUILDING = false;
            log.error("服务实例：{} 正在关闭，退出构建！", SysConstants.hostName);
            throw new BusinessException(ServerCode.SERVER_CLOSING);
        }
        //开始计时
        StopWatch watch = new StopWatch();
        watch.start();

        BuildLog buildLog = new BuildLog();
        final BuildSceneResultBean buildSceneResult = new BuildSceneResultBean();
        Future<ModelingBuildStatus> future = null;
        try {

            //休眠2秒等待准备数据
            Thread.sleep(2000L);

            //计算前准备
            preBuild(message, buildLog);

            future = SysConstants.executorService.submit(()->{
                return buildScene(message, buildSceneResult);
            });
            future.get(SysConstants.modelTimeOut, TimeUnit.HOURS);

            //结束计时
            watch.stop();
            buildLog.setDuration(watch.getTime(TimeUnit.SECONDS));
        } catch (TimeoutException ex) {
            log.error("服务实例：{} 构建异常：",SysConstants.hostName,ex);
            buildSceneResult.setBuildStatus(ModelingBuildStatus.OVERTIME);
        } catch (BuildException e){
            buildSceneResult.setBuildStatus(e.getBuildStatus());
        } catch(Exception e) {
            log.error("服务实例：{} 构建异常：", SysConstants.hostName, e);
            if (e.getCause() instanceof BuildException) {
                buildSceneResult.setBuildStatus(((BuildException) e.getCause()).getBuildStatus());
            } else {
                buildSceneResult.setBuildStatus(ModelingBuildStatus.FAILED);
                //未知异常，记录异常信息
                buildLog.setReason(ExceptionUtil.stacktraceToString(e, 3000));
            }
        }
        //计算后处理
        afterBuild(message, buildSceneResult, buildLog);

    }

    private ModelingBuildStatus buildScene(BuildSceneCallMessage message, BuildSceneResultBean buildSceneResult) throws Exception{
        Long computeTime = null;//计算耗时
        String num = null;
        //如果mq生产者在消息体中设置了结算结果mq队列名，就发到这个队列，否则就发送到默认队列
        String resultQueueName = StrUtil.isNotBlank(message.getResultReceiverMqName()) ?
            message.getResultReceiverMqName() : queueModelingPost;
        //支付状态
        int payStatus = 0;
        String unicode = message.getUnicode();
        String path = message.getPath();
        String prefix = message.getPrefix();
        num = message.getSceneNum();
        //不同的相机不同的方法
        String cameraType = message.getCameraType();
        String algorithm = message.getAlgorithm();
        String fileId = message.getFileId();
        String cameraName = message.getCameraName();
        //0表示有4k图，1表示没有
        String resolution = message.getResolution();
        //判断调用V2还是V3版本的算法
        String buildType = ObjectUtils.isEmpty(message.getBuildType()) ? "V2" : message.getBuildType();

        String dataPath = "data/data" + num;//老版本文件路径

        Map<String,String> map = null;
        Integer pushChannel = null;
        String pushToken = null;
        Integer videoVersion = null;

        buildSceneResult.setResultQueueName(resultQueueName);
        buildSceneResult.setCameraType(cameraType);
        buildSceneResult.setNum(num);


        Map<String, String> dataMap = ComputerUtil.getTypeString(cameraType, algorithm,
            resolution);
        String splitType = dataMap.get("splitType");
        String skyboxType = dataMap.get("skyboxType");
        String dataDescribe = dataMap.get("dataDescribe");

        log.info("用的算法是："+algorithm);
        log.info("用的相机是："+ (Integer.parseInt(cameraType) < 4 ? "单球目" : "双球目（八目）"));

        //4表示硬件部研发的双球目相机，其余为旧版本相机
        if(Integer.parseInt(cameraType) < 3){

            String data = FileUtils.readFile(path + File.separator + "capture" +File.separator+"data.fdage");
            JSONObject dataJson = new JSONObject();
            if(data!=null){
                dataJson = JSONObject.parseObject(data);
                if(dataJson.containsKey("imgs")){
                    splitType = "SPLIT_V4";
                }
            }
            //生成project.json和data.json供算法部使用
            ComputerUtil.createJson(path, splitType, skyboxType, dataDescribe, num, path);
            //计算模型并返回需要上传oss的文件集合
            map = ComputerUtil.computer(num, path, buildType);

        } else {

            path = SceneUtil.getPath(path, cameraName, fileId, Integer.parseInt(cameraType), unicode);

            String dataFdagePath = path + File.separator + "capture" +File.separator+"data.fdage";
            log.info("dataFdagePath 文件路径 ：{}", dataFdagePath);
            String data = FileUtils.readFile(dataFdagePath);
            //获取data.fdage的内容
            JSONObject dataJson = new JSONObject();
            if(data!=null){
                dataJson = JSONObject.parseObject(data);
            }
            pushChannel = dataJson.getInteger("pushChannel");
            pushToken = dataJson.getString("pushToken");

            /*
             1、判断是否计算过资源，若计算过删除缓存, 如果caches中存在_images文件或者目录，就删除掉，否则就删除除了images以外的所有文件和目录
             2、删除上一次计算出来的result目录
             */
            this.deleteCachesAndResult(path);

            //v2版本使用4k算法
            if(dataJson.containsKey("videoVersion")
                && StringUtils.isNotEmpty(dataJson.getString("videoVersion"))
                && Integer.parseInt(dataJson.getString("videoVersion")) < 4){
                skyboxType = "SKYBOX_V6";
            }

            //生成project.json和data.json并写到path目录下供算法部使用
            log.info("path:" + path);
            ComputerUtil.createJson(path, splitType, skyboxType, dataDescribe, num, path);
            //计算模型并返回需要上传oss的文件集合
            map = ComputerUtil.computer(num, path, buildType);
            //计算完成时间
            log.info("计算完成耗时：{}",computeTime);

            if(Integer.parseInt(cameraType) == 5 || Integer.parseInt(cameraType) == 6){
                map.put(path + File.separator + "capture/stitch_params.txt", dataPath + "/stitch_params.txt");
            }
            map.put(path + File.separator + "capture/Up.xml", dataPath + "/Up.xml");
            map.put(path + File.separator + "capture/Up2.xml", dataPath + "/Up2.xml");

            //转台相机
            if(Integer.parseInt(cameraType) == 13){
                map.put(path + File.separator + "capture/Up.txt", dataPath + "/Up.txt");
                map.put(path + File.separator + "capture/Up2.txt", dataPath + "/Up2.txt");
            }

            //计算已使用容量
            payStatus = 1;

            log.info("八目上完oss结束修改数据："+num);

            //读取计算结果文件生成videosJson
            String videoVersionStr = dataJson.getString("videoVersion");
            videoVersion = StrUtil.isEmpty(videoVersionStr) ? null : Integer.parseInt(videoVersionStr);

            //计算成功  激光转台相机 同步 请求
            this.jgSynch(message);
        }

        buildSceneResult.setBuildStatus(ModelingBuildStatus.SUCCESS);
        buildSceneResult.setFileId(fileId);
        buildSceneResult.setPayStatus(payStatus);
        buildSceneResult.setUploadMap(map);
        buildSceneResult.setPath(path);
        buildSceneResult.setPushChannel(pushChannel);
        buildSceneResult.setPushToken(pushToken);
        buildSceneResult.setPrefix(prefix);
        buildSceneResult.setVideoVersion(videoVersion);

        CreateObjUtil.deleteFile(path.replace(ConstantFilePath.BUILD_MODEL_PATH, "/") + "/capture");

        return ModelingBuildStatus.SUCCESS;
    }

    private void afterBuild(BuildSceneCallMessage message, BuildSceneResultBean buildSceneResult, BuildLog buildLog){
        try {
            ModelingBuildStatus buildStatus = buildSceneResult.getBuildStatus();
            String buildLogPath = String.format(UploadFilePath.BUILD_LOG_PATH, message.getSceneNum());
            log.info("服务{} 计算结束:{},计算状态：{}", SysConstants.hostName, buildLog.getNum(),buildStatus.message());

            if(!buildStatus.equals(ModelingBuildStatus.REPEAT)){
                //释放锁
                redisUtil.del(String.format(RedisKey.SCENE_BUILDING, message.getSceneNum()));

                //生成status.json文件
                this.upoadLog(message.getSceneStatus(), message.getWebSite(),
                    message.getSceneNum(),message.getThumb(),message.getPayStatus(), message.getDataSource());

                //如果是重复计算，没有走到计算逻辑，不需要上传日志文件
                buildLog.setLogDetailPath(prefixAli+ buildLogPath + "console.log");
            }
            buildLog.setBuildStatus(buildStatus.code());
            buildLog.setCameraType(Integer.parseInt(buildSceneResult.getCameraType()));
            buildLogService.updateById(buildLog);

        } catch (Exception e) {
            log.error("计算后业务处理出错！", e);
        }finally {
            //发送计算结果mq
            buildSceneResult.setDuration(buildLog.getDuration());
            this.sendCallResult(message, buildSceneResult);

            //计算完毕，将当前系统构建状态改为false
            SysConstants.SYSTEM_BUILDING = false;
        }
    }

    private void preBuild(BuildSceneCallMessage message, BuildLog buildLog) throws BuildException {

        // 初始化日志参数
        buildLog.setHostName(SysConstants.hostName);
        buildLog.setQueueName(queueModelingCall);
        buildLog.setNum(message.getSceneNum());
        buildLog.setDataSource(message.getDataSource());
        buildLogService.save(buildLog);
    }

    /**
     * 发送计算结果
     * @param message
     * @param buildSceneResult
     */
    private void sendCallResult(BuildSceneCallMessage message, BuildSceneResultBean buildSceneResult){
        ModelingBuildStatus buildStatus = buildSceneResult.getBuildStatus();
        //重复计算不需要发送mq做后置处理
        if(buildStatus.equals(ModelingBuildStatus.REPEAT)){
            return;
        }
        boolean buildSuccess = buildStatus.equals(ModelingBuildStatus.SUCCESS) ? true : false;
        rabbitMqProducer.sendByWorkQueue(queueModelingPost,
            BuildSceneResultMqMessage.builder()
                .buildSuccess(buildSuccess)
                .cameraType(buildSceneResult.getCameraType())
                .computeTime(buildSceneResult.getDuration())
                .fileId(buildSceneResult.getFileId())
                .payStatus(buildSceneResult.getPayStatus())
                .sceneCode(message.getSceneNum())
                .uploadFiles(buildSceneResult.getUploadMap())
                .path(buildSceneResult.getPath())
                .pushChannel(buildSceneResult.getPushChannel())
                .pushToken(buildSceneResult.getPushToken())
                .prefix(buildSceneResult.getPrefix())
                .videoVersion(buildSceneResult.getVideoVersion())
                .build());
        //如果是计算失败，需要发送钉钉
        if(!buildSuccess){
            // TODO: 2022/1/25 测试阶段，先注释掉推送
//            this.sendDingTalkMq(buildStatus.message(), message);
        }
    }

    private void upoadLog(Integer sceneStatus, String webSite,
        String num, String thumb, Integer payStatus, String dataSource){
        try{
            JSONObject statusJson = new JSONObject();
            //临时将-2改成1，app还没完全更新
            if(sceneStatus == -2){
                sceneStatus = 1;
            }else if(sceneStatus == -1){
                // 失败状态不予显示到客户端
                sceneStatus = 0;
            }
            statusJson.put("status", sceneStatus);
            statusJson.put("webSite", webSite);
            statusJson.put("sceneNum", num);
            statusJson.put("thumb", thumb);
            statusJson.put("payStatus", payStatus);
            FileUtils.writeFile(ConstantFilePath.SCENE_PATH+"data/data"+num+File.separator+"status.json", statusJson.toString());

            String dataPath = String.format(UploadFilePath.DATA_VIEW_PATH, num);
            String buildLogPath = String.format(UploadFilePath.BUILD_LOG_PATH, num);

            //todo 待删除old_path
            uploadToOssUtil.upload(
                ConstantFilePath.SCENE_PATH + "data/data" + num + File.separator + "status.json",
                "data/data" + num + File.separator + "status.json");

            uploadToOssUtil.upload(
                ConstantFilePath.SCENE_PATH + "data/data" + num + File.separator + "status.json",
                dataPath + "status.json");

            uploadToOssUtil.upload(dataSource + File.separator + "console.log",
                buildLogPath + "console.log");

        }catch (Exception e){
            log.error("上传日志失败！", e);
        }
    }

    private void jgSynch(BuildSceneCallMessage message){
        log.info("激光转台相机 同步 请求 ");
        if(Integer.parseInt(message.getCameraType()) != 14)
            return;

        String sceneName = message.getSceneName();
        String dataSource = message.getDataSource();
        Long userId = message.getUserId();
        String userName = message.getUserName();
        String cameraName = message.getCameraName();
        try {
            String title = "";
            if(StringUtils.isNotEmpty(sceneName)){
                title = sceneName;
            }
//                        String dataSource = sceneProExt.getDataSource();
            String jgPath = dataSource;
            //创建目录
            if(dataSource.lastIndexOf("/")!=-1){
                jgPath = jgPath + "_laserData";
            }else{
                jgPath = jgPath.substring(0,jgPath.length()-1) + "_laserData";
            }

            FileUtils.createDir(jgPath+"/extras");

            log.info("生成 激光相机目录 " + jgPath);
            //生成data.json
            JSONObject jgDataJson = new JSONObject();
            jgDataJson.put("split_type", "SPLIT_V15");
            jgDataJson.put("skybox_type", "SKYBOX_V5");
            jgDataJson.put("extras", null);
            FileUtils.writeFile(jgPath + File.separator + "data.json", jgDataJson.toString());

            CreateObjUtil.cpfile(dataSource  + "/results/laserData/cover", jgPath+"/extras/");
            CreateObjUtil.cplaserfile(dataSource  + "/results/laserData", jgPath+File.separator);

            //激光相机
            String url = laserHost+"/indoor/{sceneCode}/service/init?path="
                + jgPath + File.separator + "laserData" + "&title="+ title
                + "&childName=" + cameraName + "&createTime=" +  message.getSceneProCreateTime()
                + "&snCode="+ cameraName;

            if(userId!=null){
                url = url + "&userId=" + userId;
            }

            if(StringUtils.isNotEmpty(userName)){
                url = url + "&phone=" + userName;
            }

            url = url.replace("{sceneCode}",message.getSceneNum());
            log.info("激光转台相机 同步 ：" + url);
            JSONObject hotListJson =
                JSONObject.parseObject(restTemplate.getForObject(url,String.class));
            log.info("激光转台相机 同步结束 ：" + hotListJson);
        }catch (Exception e){
            log.error("激光转台相机同步失败", e);
            // TODO: 2022/1/25 测试阶段，先注释掉推送
//            this.sendDingTalkMq("激光转台相机同步失败", message);
        }
    }

    private void deleteCachesAndResult(String path){
        //判断是否计算过资源，若计算过删除缓存, 如果caches中存在_images文件或者目录，就删除掉，否则就删除除了images以外的所有文件和目录
        File caches = new File(path + File.separator + "caches");
        if(caches.exists()){
            for(File deleteFile : caches.listFiles()){
                if(new File(path + "_images").exists()){
                    if(deleteFile.isDirectory()){
                        FileUtils.delAllFile(deleteFile.getAbsolutePath());
                    }else {
                        FileUtils.deleteFile(deleteFile.getAbsolutePath());
                    }
                }else if(!deleteFile.getAbsolutePath().contains("images")){
                    if(deleteFile.isDirectory()){
                        FileUtils.delAllFile(deleteFile.getAbsolutePath());
                    }else {
                        FileUtils.deleteFile(deleteFile.getAbsolutePath());
                    }
                }
            }
        }
        //删除上一次计算出来的result目录
        if(new File(path + File.separator + "results").exists()){
            FileUtils.delAllFile(path + File.separator + "results");
        }
    }

    private void sendDingTalkMq(String reason,BuildSceneCallMessage message){
        String serverPath = message.getPath().substring(0,message.getPath().lastIndexOf("/")+1).concat(message.getPrefix());
        rabbitMqProducer.sendByWorkQueue(
            queueModelingDt,
            BuildSceneFailDTMqMessage.builder()
            .hostName(SysConstants.hostName)
            .reason(reason)
            .num(message.getSceneNum())
            .serverPath(serverPath)
            .build());
    }

}
