package com.qmth.themis.exam.websocket;

import com.alibaba.fastjson.JSONObject;
import com.qmth.themis.business.cache.ExamRecordCacheUtil;
import com.qmth.themis.business.constant.SpringContextHolder;
import com.qmth.themis.business.constant.SystemConstant;
import com.qmth.themis.business.dto.MqDto;
import com.qmth.themis.business.dto.WebsocketDto;
import com.qmth.themis.business.entity.TOeExamRecord;
import com.qmth.themis.business.enums.ExamRecordStatusEnum;
import com.qmth.themis.business.enums.MqTagEnum;
import com.qmth.themis.business.enums.WebsocketStatusEnum;
import com.qmth.themis.business.enums.WebsocketTypeEnum;
import com.qmth.themis.business.service.MqDtoService;
import com.qmth.themis.business.service.TOeExamRecordService;
import com.qmth.themis.business.util.*;
import com.qmth.themis.common.contanst.Constants;
import com.qmth.themis.common.enums.ExceptionResultEnum;
import com.qmth.themis.common.exception.BusinessException;
import com.qmth.themis.common.util.GsonUtil;
import com.qmth.themis.exam.listener.service.MqOeLogicService;
import com.qmth.themis.exam.websocket.interceptor.WebSocketOeHandshakeInterceptor;
import com.qmth.themis.exam.websocketTemplete.WebSocketOeMessageTemplete;
import com.qmth.themis.mq.templete.Concurrently;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Description: websocket考生端
 * @Param:
 * @return:
 * @Author: wangliang
 * @Date: 2020/7/10
 */
@ServerEndpoint(value = SystemConstant.WEBSOCKET_OE_PREFIX, configurator = WebSocketOeHandshakeInterceptor.class)
@Component
public class WebSocketOeServer implements Concurrently {

    private final static Logger log = LoggerFactory.getLogger(WebSocketOeServer.class);

    private volatile static ConcurrentHashMap<String, WebSocketOeServer> webSocketMap = new ConcurrentHashMap<>();

    /**
     * 与某个客户端的连接会话，需要通过它来给客户端发送数据
     */
    private Session session = null;

    private String deviceId = null, websocketSessionId = null;

    private Long recordId = null, updateTime = null;

    private RedisUtil redisUtil;

    private Map<String, Object> tranMap = null;

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        this.redisUtil = SpringContextHolder.getBean(RedisUtil.class);
        this.recordId = (Long) session.getUserProperties().get(Constants.HEADER_RECORD_ID);
        this.deviceId = (String) session.getUserProperties().get(Constants.HEADER_DEVICE_ID);
        this.session = session;
        session.setMaxIdleTimeout(SystemConstant.WEBSOCKET_MAX_TIME_OUT);
        websocketSessionId = String.valueOf(UidUtil.nextId());
        if (webSocketMap.containsKey(this.websocketSessionId)) {
            throw new BusinessException(ExceptionResultEnum.REPEAT_CONNECT_ERROR);
        } else {
            webSocketMap.put(this.websocketSessionId, this);
            addOnlineCount();
        }
        log.info("考试记录:{}，WS连接创建:{}，在线人数:{}", this.recordId, this.websocketSessionId, getOnlineCount());
        //修改record相关状态需要强制上锁
        String lockKey = SystemConstant.REDIS_LOCK_WEBSOCKET_PREFIX + ":client:" + recordId;
        redisUtil.waitLock(lockKey, SystemConstant.REDIS_LOCK_WEBSOCKET_TIME_OUT);
        try {
            WebsocketUtil.updateExamRecordWebsocketStatus(this.recordId, this.websocketSessionId,
                    WebsocketStatusEnum.ON_LINE);
        } finally {
            redisUtil.releaseLock(lockKey);
        }
        this.updateTime = System.currentTimeMillis();
        tranMap = WebsocketUtil.initWebsocket(this.recordId, this.deviceId, this.updateTime);
        MqUtil mqUtil = SpringContextHolder.getBean(MqUtil.class);
        MqDtoService mqDtoService = SpringContextHolder.getBean(MqDtoService.class);
        Map mqMap = new HashMap<>();
        mqMap.put(SystemConstant.RECORD_ID, this.recordId);
        //        mqMap.put(MonitorVideoSourceEnum.MOBILE_FIRST.name().toLowerCase(), ExamRecordCacheUtil.getMonitorStatus(this.recordId, MonitorVideoSourceEnum.MOBILE_FIRST));
        //        mqMap.put(MonitorVideoSourceEnum.MOBILE_SECOND.name().toLowerCase(), ExamRecordCacheUtil.getMonitorStatus(this.recordId, MonitorVideoSourceEnum.MOBILE_SECOND));
        MqDto mqDto = new MqDto(mqUtil.getTopic(),
                MqTagEnum.OE_WEBSOCKET_MOBILE_MONITOR_STATUS.name(), recordId,
                MqTagEnum.OE_WEBSOCKET_MOBILE_MONITOR_STATUS, String.valueOf(recordId), mqMap,
                String.valueOf(recordId));
        mqDtoService.assembleSendAsyncOrderMsg(mqDto);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() throws NoSuchAlgorithmException {
        log.info("onClose is come in");
        if (webSocketMap.containsKey(this.websocketSessionId)) {
            webSocketMap.remove(this.websocketSessionId);
            //从set中删除
            subOnlineCount();
            //修改record相关状态需要强制上锁
            String lockKey = SystemConstant.REDIS_LOCK_WEBSOCKET_PREFIX + ":client:" + recordId;
            redisUtil.waitLock(lockKey, SystemConstant.REDIS_LOCK_WEBSOCKET_TIME_OUT);
            try {
                //比较考试记录最新websocketId是否与当前websocketId相同
                String currentId = ExamRecordCacheUtil.getClientWebsocketId(recordId);
                if (this.websocketSessionId != null && this.websocketSessionId.equals(currentId)) {
                    long timestamp = System.currentTimeMillis();
                    WebsocketUtil.updateExamRecordWebsocketStatus(this.recordId, this.websocketSessionId, WebsocketStatusEnum.OFF_LINE);
                    //判断是否是正常退出
                    TOeExamRecordService tOeExamRecordService = SpringContextHolder.getBean(TOeExamRecordService.class);
                    tOeExamRecordService.sendExamRecordDataSaveMq(this.recordId, timestamp);
                    ExamRecordStatusEnum status = ExamRecordCacheUtil.getStatus(this.recordId);
                    if (Objects.equals(status, ExamRecordStatusEnum.ANSWERING)) {
                        //大于等于超时时间，说明规定时间内都没有通信，非正常退出，因为期间会有心跳更新updateTime
                        if ((System.currentTimeMillis() - this.updateTime) / 1000
                                >= SystemConstant.WEBSOCKET_MAX_TIME_OUT / 1000) {
                            log.info("超时退出");
                            //发送延时mq消息start
                            MqDtoService mqDtoService = SpringContextHolder.getBean(MqDtoService.class);
                            tranMap = mqDtoService.buildMqDelayMsg("2m");
                            tranMap.put(SystemConstant.RECORD_ID, this.recordId);
                            MqUtil mqUtil = SpringContextHolder.getBean(MqUtil.class);
                            MqDto mqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.OE_UN_NORMAL.name(),
                                    MqTagEnum.OE_UN_NORMAL, MqTagEnum.OE_UN_NORMAL, String.valueOf(this.recordId),
                                    this.tranMap, this.websocketSessionId);
                            mqDtoService.assembleSendAsyncDelayMsg(mqDto);
                            //发送延时mq消息end
                        } else {
                            log.info("正常退出");
                            //                    ExamConstant.sendExamStopMsg(this.recordId, true, false);
                            tOeExamRecordService.examBreakLogic(this.recordId, true);
                        }
                    }
                }
            } finally {
                redisUtil.releaseLock(lockKey);
            }
        }
        log.info("考试记录:{},WS连接关闭:{},在线人数:{}", this.recordId, this.websocketSessionId, getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        //可以群发消息
        //消息保存到数据库、redis
        if (Objects.nonNull(message) && session.isOpen()) {
            try {
                //解析发送的报文
                JSONObject jsonObject = JSONObject.parseObject(message);
                log.info("onMessage:{}", jsonObject.toJSONString());
                if (Objects.nonNull(jsonObject)) {
                    WebSocketOeMessageTemplete webSocketOeMessageTemplete = SpringContextHolder
                            .getBean(WebSocketOeMessageTemplete.class);
                    TOeExamRecordService tOeExamRecordService = SpringContextHolder.getBean(TOeExamRecordService.class);
                    MqUtil mqUtil = SpringContextHolder.getBean(MqUtil.class);
                    MqDtoService mqDtoService = SpringContextHolder.getBean(MqDtoService.class);

                    WebsocketDto websocketDto = GsonUtil.fromJson(GsonUtil.toJson(jsonObject), WebsocketDto.class);
                    jsonObject.getJSONObject(SystemConstant.BODY).put(SystemConstant.RECORD_ID, this.recordId);
                    websocketDto.setBody(jsonObject.getJSONObject(SystemConstant.BODY));
                    //2022.04.13 补发OE_WEBSOCKET_EXAM_STOP消息
                    ExamRecordStatusEnum statusEnum = ExamRecordCacheUtil.getStatus(this.recordId);
                    if (Objects.isNull(statusEnum)) {
                        TOeExamRecord tOeExamRecord = tOeExamRecordService.getById(this.recordId);
                        statusEnum = tOeExamRecord.getStatus();
                    }
                    if (Objects.nonNull(statusEnum) && (statusEnum == ExamRecordStatusEnum.FINISHED
                            || statusEnum == ExamRecordStatusEnum.PERSISTED || statusEnum == ExamRecordStatusEnum.JUNK)) {
                        Map<String, Object> properties = new HashMap<>();
                        properties.put(SystemConstant.REMOVE_WEBSOCKET, true);
                        MqDto clientMqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.OE_WEBSOCKET_EXAM_STOP.name(),
                                recordId, MqTagEnum.OE_WEBSOCKET_EXAM_STOP, String.valueOf(recordId), properties,
                                String.valueOf(recordId));
                        mqDtoService.assembleSendAsyncOrderMsg(clientMqDto);
                    }
                    WebsocketTypeEnum websocketTypeEnum = WebsocketTypeEnum
                            .valueOf(websocketDto.getType().toUpperCase());
                    if (Objects.nonNull(websocketTypeEnum) && (websocketTypeEnum == WebsocketTypeEnum.SYNC_STATUS
                            || websocketTypeEnum == WebsocketTypeEnum.INVIGILATE_NOTICE_ACK)) {
                        Method method = webSocketOeMessageTemplete.getClass()
                                .getDeclaredMethod(websocketTypeEnum.getDesc(), String.class, Long.class);
                        WebsocketDto result = (WebsocketDto) method
                                .invoke(webSocketOeMessageTemplete, String.valueOf(websocketDto.getBody()),
                                        websocketDto.getTime());
                        this.sendMessage(result);
                    }
                }
            } catch (Exception e) {
                log.error(SystemConstant.LOG_ERROR, e);
                if (log.isInfoEnabled()) {
                    log.info("incoming message:" + message);
                }
            }
        }
    }

    /**
     * 错误
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) throws IOException {
        log.error("考试记录{},WS连接错误:{},原因:{}", this.recordId, this.websocketSessionId, error.getMessage());
        close(this);
        throw new BusinessException(error.getMessage());
    }

    /**
     * 实现服务器主动推送
     *
     * @param message
     * @throws IOException
     */
    public void sendMessage(Object message) {
        log.info("message:{}", JacksonUtil.parseJson(message));
        this.session.getAsyncRemote().sendText(JacksonUtil.parseJson(message));
        this.updateTime = System.currentTimeMillis();
    }

    /**
     * 获取在线人数
     *
     * @return
     */
    public synchronized int getOnlineCount() {
        Object o = redisUtil.get(SystemConstant.WEBSOCKET_OE_ONLINE_COUNT);
        return Objects.isNull(o) ? 0 : (int) o;
    }

    /**
     * 在线人数加一
     */
    public synchronized void addOnlineCount() {
        redisUtil.increment(SystemConstant.WEBSOCKET_OE_ONLINE_COUNT);
    }

    /**
     * 在线人数减一
     */
    public synchronized void subOnlineCount() {
        redisUtil.decrement(SystemConstant.WEBSOCKET_OE_ONLINE_COUNT);
    }

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                    ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        MqOeLogicService mqOeLogicService = SpringContextHolder.getBean(MqOeLogicService.class);
        try {
            long threadId = Thread.currentThread().getId();
            String threadName = Thread.currentThread().getName();
            for (MessageExt messageExt : msgs) {
                log.info(":{}-:{} websocket oe Consumer重试次数:{}", threadId, threadName, messageExt.getReconsumeTimes());
                MqDto mqDto = JacksonUtil.readJson(new String(messageExt.getBody(), Constants.CHARSET), MqDto.class);
                log.info(":{}-:{} websocket oe Consumer接收到的消息:{}", threadId, threadName, JacksonUtil.parseJson(mqDto));
                mqOeLogicService.execMqOeLogic(mqDto, SystemConstant.MQ_BROADCAST_TOPIC_BUFFER_LIST);
//                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        } catch (Exception e) {
            log.error("mq websocket oe，消息消费出错", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;//重试
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;//成功
    }

    public static ConcurrentHashMap<String, WebSocketOeServer> getWebSocketMap() {
        return webSocketMap;
    }

    /**
     * 关闭session
     *
     * @param webSocketOeServer
     */
    public static void close(WebSocketOeServer webSocketOeServer) throws IOException {
        //判断当前连接是否还在线
        if (Objects.nonNull(webSocketOeServer) && Objects.nonNull(webSocketOeServer.session)
                && webSocketOeServer.session.isOpen()) {
            webSocketOeServer.session
                    .close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, SystemConstant.WEBSOCKET_CLOSE));
        }
    }

    public Long getRecordId() {
        return recordId;
    }
}