package com.qiuguo.iot.box.websocket.api.handler;

import cn.hutool.crypto.digest.MD5;
import com.alibaba.fastjson.JSONObject;
import com.qiuguo.iot.base.annotation.WebSocketMapping;
import com.qiuguo.iot.base.constans.RedisConstans;
import com.qiuguo.iot.base.enums.DeviceCodeEnum;
import com.qiuguo.iot.base.enums.DeviceTypeEnum;
import com.qiuguo.iot.base.enums.YesNo;
import com.qiuguo.iot.base.model.UserDeviceInfoModel;
import com.qiuguo.iot.box.websocket.api.domain.BaseSession;
import com.qiuguo.iot.box.websocket.api.domain.box.BoxTalkMessage;
import com.qiuguo.iot.box.websocket.api.filter.LogMdcConfiguration;
import com.qiuguo.iot.box.websocket.api.filter.LogWebFilter;
import com.qiuguo.iot.data.entity.device.DeviceInfoEntity;
import com.qiuguo.iot.data.entity.device.DeviceUserBindEntity;
import com.qiuguo.iot.data.request.device.DeviceInfoRequest;
import com.qiuguo.iot.data.request.device.DeviceUserBindRequest;
import com.qiuguo.iot.data.service.device.DeviceInfoService;
import com.qiuguo.iot.third.nlp.action.Actions;
import com.qiuguo.iot.third.service.NlpService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.ReactiveValueOperations;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.socket.*;
import reactor.core.publisher.*;

import javax.annotation.Resource;

@Component
@WebSocketMapping("/websocket/box")
@Slf4j
public class BoxWebSocketHandler extends BaseWebSocketProcess implements WebSocketHandler {
    @Value("${device.checkTimeout}")
    private Boolean checkTimeout;
    @Value("${device.timeout}")
    private Long timeOut;//2分钟

    @Resource
    private ReactiveStringRedisTemplate reactiveStringRedisTemplate;

    @Resource
    private DeviceInfoService deviceInfoService;


    @Resource
    private NlpService nlpService;





    @Override
    public Mono<Void> handle(WebSocketSession session) {
        // 在生产环境中，需对url中的参数进行检验，如token，不符合要求的连接的直接关闭
        HandshakeInfo handshakeInfo = session.getHandshakeInfo();
        HttpHeaders headers = handshakeInfo.getHeaders();
        String sn = headers.get("sn").get(0);
        Long linkTime = Long.parseLong(headers.get("time").get(0));
        if(checkTimeout && System.currentTimeMillis() - linkTime > timeOut){
            //关闭连接
            log.info("设备{}，请求数据已超时", sn);
            return session.close();
        }
        String ip = headers.get(LogWebFilter.HEAD_IP).get(0);
        String signature = headers.get("signature").get(0);
        Long userId = Long.parseLong(headers.get("userId").get(0));


        //校验
        checkToken(sn, linkTime, signature, userId);
        //

       log.info("登录成功SN:{}", sn);

        Mono<Void> input = session.receive().map(webSocketMessage ->{
            //MDC.put(LogMdcConfiguration.PRINT_LOG_ID, getBoxSessionWithSn().getLogId());
            String text = webSocketMessage.getPayloadAsText();
            log.info("设备端收到消息：{}", text);
            BoxTalkMessage boxTalkMessage = JSONObject.parseObject(text, BoxTalkMessage.class);
            nlpService.getActionWithLacSingle(boxTalkMessage.getMessage()).defaultIfEmpty(new Actions()).map(actions -> {
                BaseSession boxSession = getBoxSessionWithSn(boxTalkMessage.getSn());
                //处理
                if(actions.getActions() == null || actions.getActions().size() == 0){
                    //调用千问回答
                    log.info("暂时无法理解，我还在努力学习中");
                }else if(boxSession == null){
                    log.info("未匹配到用户session，可能传错用户id");
                    session.close().subscribe();
                }else{
                    processAction(actions, userId, boxSession);
                }

                return Mono.empty();
            }).subscribe();

            log.info("收到SN:{}，消息：{}", boxTalkMessage.getSn(), boxTalkMessage.getMessage());
            //MDC.remove(LogMdcConfiguration.PRINT_LOG_ID);
            return Mono.empty();
        }).then();
        BaseSession boxSession = new BaseSession();
        boxSession.setSn(sn);
        boxSession.setCustomerIP(ip);
        boxSession.setSession(session);
        boxSession.setUserId(userId);
        boxSession.setLogId(MDC.get(LogMdcConfiguration.PRINT_LOG_ID));
        boxGroup.put(sn, boxSession);
        Mono<Void> output = session.send(Flux.create(sink -> boxSession.setSink(sink))).then();

//          Mono.zip() 会将多个 Mono 合并为一个新的 Mono，任何一个 Mono 产生 error 或 complete 都会导致合并后的 Mono
//          也随之产生 error 或 complete，此时其它的 Mono 则会被执行取消操作。

        return Mono.zip(input, output).doFinally(signalType -> {
            // MDC.put(LogMdcConfiguration.PRINT_LOG_ID, requestId);
            boxGroup.remove(boxSession.getSn());//断链后及时移除
            log.info("设备断开连接SN：{}", boxSession.getSn());
            ReactiveValueOperations<String, String> operations = reactiveStringRedisTemplate.opsForValue();
            UserDeviceInfoModel userDeviceInfoModel = new UserDeviceInfoModel();
            userDeviceInfoModel.setStatus(YesNo.NO.getCode());
            userDeviceInfoModel.setUserId(userId);
            userDeviceInfoModel.setSn(sn);
            operations.set(RedisConstans.USER_BOX_INFO + userId,
                    JSONObject.toJSONString(userDeviceInfoModel),
                    RedisConstans.TEN_YEAR).subscribe();
            //  MDC.remove(LogMdcConfiguration.PRINT_LOG_ID);
        }).then();
    }



    private void checkToken(String sn, Long linkTime, String signature, Long userId){
        ReactiveValueOperations<String, String> operations = reactiveStringRedisTemplate.opsForValue();
        operations.get(RedisConstans.DEVICE_INFO + sn).defaultIfEmpty("").flatMap(s -> {
            if(com.qiuguo.iot.base.utils.StringUtils.isNotBlank(s)){
                try{
                    DeviceInfoEntity dv = JSONObject.parseObject(s, DeviceInfoEntity.class);
                    return Mono.just(dv);
                }catch (Exception e){
                    log.info("转换异常，清除redis。下次连接成功{}", e);
                    //清除异常redis
                    operations.set(RedisConstans.DEVICE_INFO + sn, "").subscribe();//不需要时间
                    BaseSession boxSession = getBoxSessionWithSn(sn);
                    if(boxSession != null){
                        boxSession.getSink().next(boxSession.getSession().textMessage("异常，请重新登录"));
                        boxSession.getSession().close().subscribe();
                    }
                }
            }
            DeviceInfoRequest request = new DeviceInfoRequest();
            request.setSn(sn);
            return deviceInfoService.selectDeviceInfoByRequest(request).defaultIfEmpty(new DeviceInfoEntity()).map(dv -> {
                if(dv.getId() != null){
                    operations.set(RedisConstans.DEVICE_INFO + dv.getSn(), JSONObject.toJSONString(dv), RedisConstans.ONE_HOUR).subscribe();//直接提交订阅
                }

                return dv;
            });
        }).map(dv ->{
            String snMd5 = MD5.create().digestHex(sn).toUpperCase();
            String wifiMd5 = MD5.create().digestHex(dv.getWifiMac()).toUpperCase();
            String btMd5 = MD5.create().digestHex(dv.getBtMac()).toUpperCase();
            String signalMd5 = MD5.create().digestHex(snMd5 + wifiMd5 + btMd5 + linkTime + dv.getKey()).toUpperCase();
            BaseSession boxSession = getBoxSessionWithSn(sn);
            if(!signalMd5.equals(signature)){
                log.info("设备{}，验签失败", sn);
                //session.send(session.textMessage(""));

                if(boxSession != null){
                    boxSession.getSink().next(boxSession.getSession().textMessage("验签失败"));
                    boxSession.getSession().close().subscribe();
                }

            }else{
                log.info("设备{}，验签成功", sn);
                if(boxSession != null){
                    boxSession.setDeviceId(dv.getId());
                }
                bindBox(dv, userId);
            }
            return Mono.empty();
        }).subscribe();
    }

    private void bindBox(DeviceInfoEntity dv, Long userId){

        log.info("开始绑定设备userId:{}， SN：{}", userId, dv);
        DeviceUserBindRequest request = new DeviceUserBindRequest();
        request.setUserId(userId);
        request.setDeviceId(dv.getId());

        deviceUserBindService.selectDeviceUserBindByRequest(request)
                .defaultIfEmpty(new DeviceUserBindEntity())
                .map(entity ->{
                    if(entity.getId() == null){
                        entity.setUserId(userId);
                        entity.setDeviceId(dv.getId());
                        //设置未主设备
                        entity.setIsMain(YesNo.YES.getCode());
                        entity.setOtherDeviceId(dv.getSn());
                        entity.setCategoryCode(DeviceCodeEnum.BOX.getName());
                        deviceUserBindService.setNoMain(userId, DeviceTypeEnum.GUO_BOX.getCode()).defaultIfEmpty(0).map(m ->{
                            log.info("解除历史isMain标注个数{}", m);
                            deviceUserBindService.insertDeviceUserBind(entity).map(l ->{
                                log.info("绑定成功SN：{} userId:{}", dv, userId);
                                //下面所有的以前未主设备改成非主设备
                                ReactiveValueOperations<String, String> operations = reactiveStringRedisTemplate.opsForValue();
                                UserDeviceInfoModel userDeviceInfoModel = new UserDeviceInfoModel();
                                userDeviceInfoModel.setStatus(YesNo.YES.getCode());
                                userDeviceInfoModel.setUserId(userId);
                                userDeviceInfoModel.setSn(entity.getOtherDeviceId());
                                operations.set(RedisConstans.USER_BOX_INFO + userId,
                                        JSONObject.toJSONString(userDeviceInfoModel),
                                        RedisConstans.TEN_YEAR).subscribe();
                                return Mono.empty();
                            }).subscribe();
                            return Mono.empty();
                        }).subscribe();

                    }
                    return Mono.empty();
                }).subscribe();

    }



}
