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.*;
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.WebSocketMobileHandshakeInterceptor;
import com.qmth.themis.exam.websocketTemplete.WebSocketMobileMessageTemplete;
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.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_MOBILE_PREFIX, configurator = WebSocketMobileHandshakeInterceptor.class)
@Component
public class WebSocketMobileServer implements Concurrently {

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

    private volatile static ConcurrentHashMap<String, WebSocketMobileServer> 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;

    private MonitorVideoSourceEnum source = 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.source = (MonitorVideoSourceEnum) session.getUserProperties().get(Constants.HEADER_SOURCE);
        this.session = session;
        session.setMaxIdleTimeout(SystemConstant.WEBSOCKET_MAX_TIME_OUT);
        this.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());
//        InetSocketAddress addr = (InetSocketAddress) WebsocketUtil
//                .getFieldInstance(this.session.getAsyncRemote(), "base#socketWrapper#socket#sc#remoteAddress");
//        this.ip = addr.toString().replace("/", "").split(":")[0];
//        log.info("ip[:{}]连接成功", this.ip);
        //修改record相关状态需要强制上锁
        String lockKey = SystemConstant.REDIS_LOCK_WEBSOCKET_PREFIX + ":" + this.source.name() + ":" + recordId;
        redisUtil.waitLock(lockKey, SystemConstant.REDIS_LOCK_WEBSOCKET_TIME_OUT);
        try {
            if (this.source == MonitorVideoSourceEnum.MOBILE_FIRST) {
                WebsocketUtil.updateExamRecordMobileFirstWebsocketStatus(this.recordId, this.websocketSessionId,
                        WebsocketStatusEnum.ON_LINE);
            } else if (this.source == MonitorVideoSourceEnum.MOBILE_SECOND) {
                WebsocketUtil.updateExamRecordMobileSecondWebsocketStatus(this.recordId, this.websocketSessionId,
                        WebsocketStatusEnum.ON_LINE);
            }
        } finally {
            redisUtil.releaseLock(lockKey);
        }
        this.updateTime = System.currentTimeMillis();
        tranMap = WebsocketUtil.initWebsocket(recordId, deviceId, updateTime);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        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 + ":" + this.source.name() + ":" + recordId;
            redisUtil.waitLock(lockKey, SystemConstant.REDIS_LOCK_WEBSOCKET_TIME_OUT);
            try {
                //判断是否是正常退出
                long timestamp = System.currentTimeMillis();
                //比较考试记录最新websocketId是否与当前websocketId相同
                String currentId = ExamRecordCacheUtil.getMobileWebsocketId(recordId, this.source);
                if (this.websocketSessionId != null && this.websocketSessionId.equals(currentId)) {
                    ExamRecordCacheUtil
                            .setMonitorStatus(this.recordId, this.source, MonitorStatusSourceEnum.STOP, timestamp);
                    ExamRecordCacheUtil
                            .setMobileWebsocketStatus(this.recordId, this.source, WebsocketStatusEnum.OFF_LINE,
                                    timestamp);
                    TOeExamRecordService tOeExamRecordService = SpringContextHolder.getBean(TOeExamRecordService.class);
                    tOeExamRecordService.sendExamRecordDataSaveMq(this.recordId, timestamp);
                    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(Constants.HEADER_SOURCE, this.source.name());

                    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)) {
                        //监控结束
                        MqDto mqDtoStop = new MqDto(mqUtil.getTopic(), MqTagEnum.MONITOR_STOP.name(),
                                recordId, MqTagEnum.MONITOR_STOP, String.valueOf(recordId), mqMap,
                                String.valueOf(recordId));
                        mqDtoService.assembleSendAsyncOrderMsg(mqDtoStop);
                    }
                }
            } 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)) {
                    WebSocketMobileMessageTemplete webSocketMobileMessageTemplete = SpringContextHolder
                            .getBean(WebSocketMobileMessageTemplete.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 补发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 mobileMqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.EXAM_STOP.name(),
                                recordId, MqTagEnum.EXAM_STOP, String.valueOf(recordId), properties,
                                String.valueOf(recordId));
                        mqDtoService.assembleSendAsyncOrderMsg(mobileMqDto);
                    }
                    WebsocketTypeEnum websocketTypeEnum = WebsocketTypeEnum
                            .valueOf(websocketDto.getType().toUpperCase());
                    if (Objects.nonNull(websocketTypeEnum) && websocketTypeEnum == WebsocketTypeEnum.SYNC_STATUS) {
                        Method method = webSocketMobileMessageTemplete.getClass()
                                .getDeclaredMethod(websocketTypeEnum.getDesc(), String.class, Long.class);
                        WebsocketDto result = (WebsocketDto) method
                                .invoke(webSocketMobileMessageTemplete, String.valueOf(websocketDto.getBody()),
                                        websocketDto.getTime());
                        this.sendMessage(result);
                    }
                }
            } catch (Exception e) {
                log.error(SystemConstant.LOG_ERROR, e);
            }
        }
    }

    /**
     * 错误
     *
     * @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_MOBILE_ONLINE_COUNT);
        return Objects.isNull(o) ? 0 : (int) o;
    }

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

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

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

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

    public Long getRecordId() {
        return recordId;
    }

    @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.execMqOeMobileLogic(mqDto, SystemConstant.MQ_BROADCAST_TOPIC_BUFFER_LIST);
//                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        } catch (Exception e) {
            log.error("mq websocket mobile，消息消费出错", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;//重试
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;//成功
    }
}