package com.ym.im.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.ym.im.entity.*;
import com.ym.im.entity.base.ChannelAttributeKey;
import com.ym.im.entity.base.NettyConstant;
import com.ym.im.entity.model.IdModel;
import com.ym.im.handler.ChannelGroupHandler;
import com.ym.im.service.*;
import com.ym.im.util.HttpClientUtils;
import com.ym.im.util.JsonUtils;
import com.ym.im.validation.group.ChatRecordReceiveGroup;
import com.ym.im.validation.group.ChatRecordSaveGroup;
import com.ym.im.validation.group.ChatRecordSendGroup;
import com.ym.im.validation.group.MsgBodyGroup;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.validation.Valid;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static com.ym.im.entity.ChatRecord.SEND;

/**
 * @author: JJww
 * @Date:2019-05-21
 */
@Slf4j
@Service
@Validated
public class UserSingleChatServiceImpl implements ChatService {

    @Autowired
    private ChannelGroupHandler channelGroup;

    @Resource(name = "myRedisTemplate")
    private RedisTemplate redisTemplate;

    @Autowired
    private StaffService staffService;

    @Autowired
    private MsgBodyService msgBodyService;

    @Autowired
    private ChatRecordService chatRecordService;

    @Autowired
    private SessionListService sessiontListService;

    @Value("${serviceUrl.user-info-url}")
    private String USER_INFO_URL;

    @Override
    public void init(ChannelHandlerContext ctx) throws IOException {

        final Long userId = ctx.channel().attr(ChannelAttributeKey.ROLE_ID).get();
        final String token = ctx.channel().attr(ChannelAttributeKey.TOKEN_INFO).get();
        final UserSocketInfo userSocketInfo = new UserSocketInfo();
        userSocketInfo.setUserId(userId);
        userSocketInfo.setChannel((NioSocketChannel) ctx.channel());
        userSocketInfo.setCol(ctx.channel().attr(ChannelAttributeKey.COL_INFO).get());
        userSocketInfo.setToken(token);
        userSocketInfo.setSessionList(this.getSessionList(userId));
        Map<String, String> headers = new HashMap<>();
        headers.put("token", token);
        final AppUser appUser = JsonUtils.json2Obj(HttpClientUtils.doGet(USER_INFO_URL, null, headers), AppUser.class);//获取deviceToken
        userSocketInfo.setDeviceTokenParam(new DeviceTokenParam().setDeviceToken(appUser.getDeviceToken()).setDeviceType(appUser.getDeviceType()));
        channelGroup.USER_GROUP.put(userId, userSocketInfo);
        this.restoreBindingRelationship(userId);      //恢复历史绑定关系
        this.broadcastUserOnline(userId);        //通知客服 用户上线
        log.info("用户: " + userId + " 上线");
    }


    @Override
    public void offline(ChannelHandlerContext ctx) {
        final Long userId = ctx.channel().attr(ChannelAttributeKey.ROLE_ID).get();
        final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(userId);
        if (userSocketInfo != null && ctx.channel().attr(ChannelAttributeKey.TOKEN_INFO).get().equals(userSocketInfo.getToken())) {
            final Map<Long, Long> staffIds = userSocketInfo.getStaffIds();
            if (staffIds.size() != 0) {
                redisTemplate.opsForHash().put(NettyConstant.IM_USERS, userId, staffIds);    // 保存最后的客服
            }
            ctx.close();
            channelGroup.USER_GROUP.remove(userId);
            log.info("用户: " + userId + " 下线");
        }
    }


    @Override
    @Validated({MsgBodyGroup.class, ChatRecordSendGroup.class})
    public NioSocketChannel distribution(Long id, @Valid MsgBody<ChatRecord> msgBody) {

        final Long merchantId = msgBody.getData().getMerchantId();
        // 获取服务用户的客服Id
        final Long staffId = channelGroup.USER_GROUP.get(id).getStaffId(merchantId);
        // 客服SocketInfo对象
        StaffSocketInfo staffSocketInfo;
        // 若客服Id为空，分配客服，不为空则获取客服SocketInfo
        if (staffId == null) {
            staffSocketInfo = staffService.getIdleStaff(merchantId, id);
            if (staffSocketInfo == null) {
                return null;
            }
        } else {
            // 根据客服id获取SocketInfo
            staffSocketInfo = channelGroup.getMerchantStaff(staffId);
            // 服务用户的客服不在线
            if (staffSocketInfo == null) {
                // Redis是否存在当前客服服务用户Set对象（TTL=60秒）
                Set<Long> members = redisTemplate.opsForSet().members(NettyConstant.STAFF_USERIDS_KEY + staffId);
                // 客服下线超过60秒，当前客服服务用户Set对象已过期，重新分配客服
                // 过期后set对象不会为空而是大小为0
                if (members.size() == 0) {
                    staffSocketInfo = staffService.getIdleStaff(merchantId, id);
                }
                return null;
            } else {
                /*
                 * 解决客服下线超过一分钟后再上线收到下线前服务的用户（用户没有再分配新客服情况）发来的消息，
                 * 客服无法回复用户的问题，
                 * 无论该用户Id是否存在于客服服务用户Set内，都向Set添加用户ID
                 */
                staffSocketInfo.getUserIds().add(id);
            }
        }
        return staffSocketInfo.getChannel();
    }


    @Override
    @Validated({MsgBodyGroup.class, ChatRecordSendGroup.class})
    public void save(Long id, @Valid MsgBody<ChatRecord> msgBody) {

        // 设置聊天基本信息
        final ChatRecord record = msgBody.getData()
                .setId(IdWorker.getId())
                .setUserId(id)
                .setSendReceive(SEND)
                .setCreateTime(new Date());
        /*
         * 设置客服Id：
         *  情况1：当前用户无客服服务，且无客服可分配，ChatRecord.staffId = null;
         *  情况2：当前用户无客服服务，且有客服可分配，ChatRecord.staffId = StaffSocketInfo.id;
         *  情况3：当前用户有客服服务，且客服在线，ChatRecord.staffId = StaffSocketInfo.id;
         *  情况3：当前用户有客服服务，且客服已下线并不超过60秒，CharRecord.staffId = staffId;
         *  情况4：当前用户有客服服务，且客服已下线并超过60秒，重新分配客服且有客服可分配，ChatRecord.staffId = StaffSocketInfo.id;
         *  情况5（特殊：需求未定暂时这样处理）：当前用户有客服服务，且客服已下线并超过60秒，重新分配客服且无客服可分配，ChatRecord.staffId = staffId;
         */
//        record.setStaffId(channelGroup.USER_GROUP.get(id).getStaffId(record.getMerchantId()));
        // 先保存至数据库，再发送消息（若颠倒顺序可能导致数据未保存，更新已读操作先执行导致消息一直是未读状态）
        chatRecordService.insertSelective(record);
        this.updateMerchantList(msgBody);
        log.info("用户 消息保存:" + record.getId());
    }

    /**
     * 发送消息至客服端
     *
     * @param msgBody 消息对象
     * @throws JsonProcessingException e
     */
    @Override
    public void send(NioSocketChannel channel, MsgBody<ChatRecord> msgBody) throws JsonProcessingException {
        msgBodyService.sendAndAck(channel, msgBody);
    }


    @Override
    @Validated({MsgBodyGroup.class, ChatRecordReceiveGroup.class, ChatRecordSaveGroup.class})
    public void ack(@Valid MsgBody<ChatRecord> msgBody) throws JsonProcessingException {
        final ChatRecord record = msgBody.getData();
        record.setModifyTime(new Date());
        chatRecordService.updateReceiveTime(record);
        UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(record.getUserId());
        if (userSocketInfo != null) {
            final Long merchantId = record.getMerchantId();
            final Long staffId = userSocketInfo.getStaffId(merchantId);
            final StaffSocketInfo staffSocketInfo = channelGroup.getMerchantStaff(merchantId, staffId);
            if (staffSocketInfo != null) {
                staffSocketInfo.writeAndFlush(msgBody);
            }
            redisTemplate.opsForHash().put(NettyConstant.MSG_KEY + record.getUserId(), record.getId(), JsonUtils.obj2Json(msgBody));
        }
        log.info("用户 回执:" + record.getId());
    }

    /**
     * 恢复历史绑定关系
     *
     * @param userId
     */
    private void restoreBindingRelationship(Long userId) {
        if (redisTemplate.opsForHash().hasKey(NettyConstant.IM_USERS, userId)) {
            final Map staffs = (HashMap<Long, Long>) redisTemplate.opsForHash().get(NettyConstant.IM_USERS, userId);
            staffs.forEach((mid, staffId) -> {
                final StaffSocketInfo staffSocketInfo = channelGroup.getMerchantStaff(Long.valueOf(mid.toString()), Long.valueOf(staffId.toString()));
                if (staffSocketInfo != null) {
                    staffSocketInfo.getUserIds().add(userId);
                }
            });
            channelGroup.USER_GROUP.get(userId).setStaffIds(staffs);
            redisTemplate.opsForHash().delete(NettyConstant.IM_USERS, userId);
        }
    }

    /**
     * 广播用户上线信息 并移除对应Set
     *
     * @param userId
     */
    private void broadcastUserOnline(Long userId) {
        final Map staffIds = channelGroup.USER_GROUP.get(userId).getStaffIds();
        staffIds.forEach((merchantId, staffId) -> {
            final Map<Long, StaffSocketInfo> staffSocketInfoMap = channelGroup.STAFF_GROUP.get(Long.valueOf(merchantId.toString()));
            if (staffSocketInfoMap != null) {
                staffSocketInfoMap.values().forEach(staffSocketInfo -> {
                    staffSocketInfo.getUserIds().remove(userId);
                    staffSocketInfo.writeAndFlush(new MsgBody<>().setCode(MsgBody.USERS_ONLINE).setData(new IdModel().setStaffId(Long.valueOf(staffId.toString())).setUserId(userId)));
                });
            }
        });
    }

    private Set<Long> getSessionList(Long userId) {
        final List<Session> merchantLists = sessiontListService.list(new QueryWrapper<Session>().lambda().eq(Session::getUserId, userId));
        return merchantLists.stream().map(Session::getMerchantId).collect(Collectors.toSet());
    }

    private void updateMerchantList(MsgBody<ChatRecord> msgBody) {
        final Date now = new Date();
        final ChatRecord chatRecord = msgBody.getData();
        final Long userId = chatRecord.getUserId();
        final Long merchantId = chatRecord.getMerchantId();
        final Set<Long> merchantList = channelGroup.USER_GROUP.get(userId).getSessionList();
        if (!merchantList.contains(merchantId)) {
            sessiontListService.save(new Session().setId(IdWorker.getId()).setUserId(userId).setMerchantId(merchantId).setCreateTime(now).setModifyTime(now));
            merchantList.add(merchantId);
        }
    }

}
