package com.zanxiang.game.module.sdk.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zanxiang.game.module.base.pojo.enums.BanStatusEnum;
import com.zanxiang.game.module.base.pojo.enums.GameCategoryEnum;
import com.zanxiang.game.module.base.pojo.enums.HttpStatusEnum;
import com.zanxiang.game.module.base.util.DateUtils;
import com.zanxiang.game.module.mybatis.entity.Game;
import com.zanxiang.game.module.mybatis.entity.GameExt;
import com.zanxiang.game.module.mybatis.entity.User;
import com.zanxiang.game.module.mybatis.entity.UserToken;
import com.zanxiang.game.module.mybatis.mapper.UserTokenMapper;
import com.zanxiang.game.module.sdk.constant.RedisKeyConstant;
import com.zanxiang.game.module.sdk.enums.ExpireTimeEnum;
import com.zanxiang.game.module.sdk.enums.LoginTypeEnum;
import com.zanxiang.game.module.sdk.enums.TokenCheckEnum;
import com.zanxiang.game.module.sdk.pojo.dto.GameAppletDTO;
import com.zanxiang.game.module.sdk.pojo.dto.UserTokenDTO;
import com.zanxiang.game.module.sdk.pojo.param.UserData;
import com.zanxiang.game.module.sdk.pojo.vo.CpTokenCheckVO;
import com.zanxiang.game.module.sdk.service.*;
import com.zanxiang.game.module.sdk.service.api.WxApiService;
import com.zanxiang.game.module.sdk.util.RedisUtil;
import com.zanxiang.game.module.sdk.util.SignUtil;
import com.zanxiang.module.redis.service.IDistributedLockComponent;
import com.zanxiang.module.util.DateUtil;
import com.zanxiang.module.util.JsonUtil;
import com.zanxiang.module.util.bean.BeanUtil;
import com.zanxiang.module.util.encryption.AESUtil;
import com.zanxiang.module.util.exception.BaseException;
import com.zanxiang.module.util.pojo.ResultVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author : lingfeng
 * @time : 2022-06-07
 * @description : 用户token管理
 */
@Slf4j
@Service
public class UserTokenServiceImpl extends ServiceImpl<UserTokenMapper, UserToken> implements IUserTokenService {

    @Value("${iapSdk.tokenSecret:unknown}")
    private String tokenSecret;

    @Autowired
    private RedisUtil<UserToken> redisUtil;

    @Autowired
    private WxApiService wxApiService;

    @Autowired
    private IGameService gameService;

    @Autowired
    private IGameExtService gameExtService;

    @Autowired
    private IIpBanService ipBanService;

    @Autowired
    private IGameAppletService gameAppletService;

    @Autowired
    private IUserService userService;

    @Autowired
    private IDistributedLockComponent distributedLockComponent;

    @Autowired
    private IUserLoginLogService userLoginLogService;

    @Override
    public ResultVO<CpTokenCheckVO> cpTokenCheckNew(String appId, Long userId, String token, String sign) {
        //查询登录签名
        GameExt gameExt = gameExtService.getByGameAppId(appId);
        if (gameExt == null || Strings.isBlank(gameExt.getLoginKey())) {
            log.error("token验证失败 , 游戏拓展信息不存在, appId : {}, userId : {}", appId, userId);
            return ResultVO.fail(TokenCheckEnum.PARAM_LACK.getMsg());
        }
        //用户信息
        User user = userService.getById(userId);
        //获取检测token
        UserToken userToken = this.getCheckUserToken(userId, token);
        //判断token是否存在, 并且没有过期
        if (userToken == null || userToken.getExpireTime() < DateUtils.localDateTimeToSecond(LocalDateTime.now())) {
            log.error("token验证失败 , token不存在或者已经失效, appId : {}, userId : {}, token : {}", appId, userId, token);
            return ResultVO.fail(TokenCheckEnum.SIGN_ERROR.getMsg());
        }
        //获取计算签名
        Tuple2<String, String> tuple2 = this.getMySign(gameExt, userId, token);
        //签名错误
        if (!Objects.equals(tuple2.getT2(), sign)) {
            log.error("token验证失败 , str : {}, mySign : {}, sign : {}", tuple2.getT1(), tuple2.getT2(), sign);
            return ResultVO.fail(TokenCheckEnum.CHECK_FAIL.getMsg());
        }
        GameAppletDTO gameAppletDTO = null;
        String wxAccessToken = null;
        try {
            gameAppletDTO = gameAppletService.getByGameId(user.getGameId());
            if (gameAppletDTO != null) {
                wxAccessToken = wxApiService.getAccessToken(gameAppletDTO.getAppId(), gameAppletDTO.getAppSecret());
            }
        } catch (Exception e) {
            log.error("获取小程序token失败，gameAppletDTO:{}", JsonUtil.toString(gameAppletDTO));
        }
        //构造返回
        return ResultVO.ok(CpTokenCheckVO.builder()
                .userId(userId)
                .appId(gameExtService.getByGameId(user.getGameId()).getAppId())
                .wxAppId(gameAppletDTO == null ? null : gameAppletDTO.getAppId())
                .wxAccessToken(wxAccessToken)
                .build());
    }

    @Override
    public ResultVO<Long> cpTokenCheck(String appId, Long userId, String token, String sign) {
        //查询登录签名
        GameExt gameExt = gameExtService.getByGameAppId(appId);
        if (gameExt == null || Strings.isBlank(gameExt.getLoginKey())) {
            log.error("token验证失败 , 游戏拓展信息不存在, appId : {}, userId : {}", appId, userId);
            return ResultVO.fail(TokenCheckEnum.PARAM_LACK.getMsg());
        }
        //获取检测token
        UserToken userToken = this.getCheckUserToken(userId, token);
        //判断token是否存在, 并且没有过期
        if (userToken == null || userToken.getExpireTime() < DateUtils.localDateTimeToSecond(LocalDateTime.now())) {
            log.error("token验证失败 , token不存在或者已经失效, appId : {}, userId : {}, token : {}", appId, userId, token);
            return ResultVO.fail(TokenCheckEnum.SIGN_ERROR.getMsg());
        }
        //获取计算签名
        Tuple2<String, String> tuple2 = this.getMySign(gameExt, userId, token);
        //签名错误
        if (!Objects.equals(tuple2.getT2(), sign)) {
            log.error("token验证失败 , str : {}, mySign : {}, sign : {}", tuple2.getT1(), tuple2.getT2(), sign);
            return ResultVO.fail(TokenCheckEnum.CHECK_FAIL.getMsg());
        }
        return ResultVO.ok(userId);
    }

    @Override
    public Tuple2<String, String> getMySign(GameExt gameExt, Long userId, String token) {
        //计算用户签名
        StringBuilder sb = new StringBuilder();
        sb.append("loginKey=").append(gameExt.getLoginKey());
        sb.append("&appId=").append(gameExt.getAppId());
        sb.append("&userId=").append(userId);
        sb.append("&token=").append(token);
        String mySign;
        try {
            mySign = SignUtil.MD5(sb.toString());
        } catch (Exception e) {
            log.error("md5工具类加密异常, str : {}, e : {}", sb.toString(), e.getMessage());
            throw new BaseException("MD5加密异常");
        }
        return Tuples.of(sb.toString(), mySign);
    }

    @Override
    public UserToken getCheckUserToken(Long userId, String token) {
        //非导量用户
        UserToken userToken = super.getOne(new LambdaQueryWrapper<UserToken>()
                .eq(UserToken::getToken, token)
                .eq(UserToken::getUserId, userId));
        if (userToken != null) {
            return userToken;
        }
        //导量处理
        List<User> relationUserList = userService.list(new LambdaQueryWrapper<User>()
                .eq(User::getRelationUserId, userId));
        //不存在导量用户
        if (CollectionUtils.isEmpty(relationUserList)) {
            return null;
        }
        Set<Long> relationUserIdSet = relationUserList.stream().map(User::getId)
                .collect(Collectors.toSet());
        return super.getOne(new LambdaQueryWrapper<UserToken>()
                .eq(UserToken::getToken, token)
                .in(UserToken::getUserId, relationUserIdSet));
    }

    @Override
    public void userTokenUpdateCheck(UserData userData) {
        //过期时间
        LocalDateTime expireTime = DateUtil.secondToLocalDateTime(userData.getExpireTime());
        //判断是否1天内过期
        if (expireTime == null || LocalDateTime.now().plusDays(1).isBefore(expireTime)) {
            return;
        }
        //更新token的时效时长
        super.update(new LambdaUpdateWrapper<UserToken>()
                .set(UserToken::getExpireTime, DateUtils.localDateTimeToSecond(LocalDateTime.now()) + ExpireTimeEnum.ONE_WEEK.getTime())
                .set(UserToken::getUpdateTime, LocalDateTime.now())
                .eq(UserToken::getUserId, userData.getUserId())
                .eq(UserToken::getToken, userData.getToken()));
        //删除token信息缓存
        redisUtil.deleteCache(this.getTokenInfoKey(userData.getToken(), userData.getDeviceType()));
        redisUtil.deleteCache(this.getUserTokenKey(userData.getUserId(), userData.getDeviceType()));
    }

    @Override
    public Boolean userTokenExpireTimeCheck(UserData userData) {
        log.error("缓存token验证请求 userData : {}", JsonUtil.toString(userData));
        //判断是否存在用户id或者token是否存在
        if (userData.getUserId() == null || Strings.isBlank(userData.getToken())) {
            return Boolean.FALSE;
        }
        //判断token在数据库是否存在
        UserToken userToken = super.getOne(new LambdaQueryWrapper<UserToken>()
                .eq(UserToken::getToken, userData.getToken())
                .eq(UserToken::getDeviceType, userData.getDeviceType()));
        //token已经更新, 返回失效
        String token = Optional.ofNullable(userToken).map(UserToken::getToken).orElse(null);
        LocalDateTime localDateTime = DateUtil.parseLocalDateTime("2025-03-11 18:45:00");
        if (Strings.isBlank(token) || userToken.getUpdateTime().isBefore(localDateTime)) {
            return Boolean.FALSE;
        }
        //验证是否被封禁
        Tuple2<Boolean, HttpStatusEnum> banCheckTuple2 = ipBanService.banCheck(userData);
        if (banCheckTuple2.getT1()) {
            return Boolean.FALSE;
        }
        //查询用户信息
        User user = userService.getOne(new LambdaQueryWrapper<User>()
                .eq(User::getId, userData.getUserId())
                .eq(User::getGameId, userData.getGameId()));
        //判断账号是否存在,或者停用
        if (user == null || Objects.equals(BanStatusEnum.BAN_STATUS.getStatus(), user.getStatus())) {
            return Boolean.FALSE;
        }
        //小程序监测sessionKey
        if (Strings.isNotBlank(user.getSessionKey())) {
            Game game = gameService.getById(userData.getGameId());
            if (Objects.equals(game.getCategory(), GameCategoryEnum.CATEGORY_WX_APPLET.getId())
                    && wxApiService.checkSessionKey(userData.getGameId(), user.getOpenId(), user.getSessionKey())) {
                return Boolean.FALSE;
            }
        }
        //token检测
        Long expireTime = userData.getExpireTime();
        if (expireTime == null) {
            return Boolean.FALSE;
        }
        //判断token是否过期
        long endTime = DateUtils.localDateTimeToSecond(LocalDateTime.now().plusDays(1));
        //判断有效期时间是否大于1天
        boolean result = expireTime > endTime;
        //token未过期, 记录登录日志
        if (result) {
            userLoginLogService.createUserLoginLog(userData, LoginTypeEnum.LOGIN_IN);
        }
        return result;
    }

    @Override
    public UserTokenDTO getTokenInfoByTokenDevice(String token, Integer deviceType) {
        //查询缓存信息
        String tokenInfoKey = this.getTokenInfoKey(token, deviceType);
        UserToken userToken = redisUtil.getCache(tokenInfoKey);
        if (userToken != null) {
            return BeanUtil.copy(userToken, UserTokenDTO.class);
        }
        //缓存不存在, 查询数据库
        userToken = super.getOne(new LambdaQueryWrapper<UserToken>()
                .eq(UserToken::getToken, token)
                .eq(UserToken::getDeviceType, deviceType));
        if (userToken == null) {
            return null;
        }
        //设置token详情缓存
        redisUtil.setCache(tokenInfoKey, userToken, ExpireTimeEnum.ONE_DAY.getTime());
        //设置用户缓存
        String userTokenKey = this.getUserTokenKey(userToken.getUserId(), deviceType);
        redisUtil.setCache(userTokenKey, userToken, ExpireTimeEnum.ONE_DAY.getTime());
        //返回数据
        return BeanUtil.copy(userToken, UserTokenDTO.class);
    }

    @Override
    public String getUserToken(Long userId, Integer deviceType) {
        //获取token
        String token = this.createToken(userId, deviceType);
        //查询数据库token信息
        UserToken userToken = super.getOne(new LambdaQueryWrapper<UserToken>()
                .eq(UserToken::getUserId, userId)
                .eq(UserToken::getDeviceType, deviceType));
        //不存在token数据, 直接创建
        if (userToken == null) {
            userToken = this.tokenSave(userId, token, deviceType);
        } else {
            //更新数据库
            userToken.setToken(token);
            userToken.setUpdateTime(LocalDateTime.now());
            userToken.setExpireTime(DateUtils.localDateTimeToSecond(LocalDateTime.now()) + ExpireTimeEnum.ONE_WEEK.getTime());
            super.updateById(userToken);
        }
        //设置用户缓存
        String userTokenKey = this.getUserTokenKey(userId, deviceType);
        redisUtil.setCache(userTokenKey, userToken, ExpireTimeEnum.ONE_DAY.getTime());
        //设置token详情缓存
        String tokenInfoKey = this.getTokenInfoKey(token, deviceType);
        redisUtil.setCache(tokenInfoKey, userToken, ExpireTimeEnum.ONE_DAY.getTime());
        //返回token
        return token;
    }

    private UserToken tokenSave(Long userId, String token, Integer deviceType) {
        String lockKey = RedisKeyConstant.TOKEN_CREATE_LOCK + userId + "_" + deviceType;
        //上锁
        if (!distributedLockComponent.doLock(lockKey, 0L, 1L, TimeUnit.MINUTES)) {
            throw new BaseException("操作频繁, 请1分钟后重试!");
        }
        try {
            UserToken userToken = UserToken.builder()
                    .userId(userId)
                    .token(token)
                    .deviceType(deviceType)
                    .createTime(LocalDateTime.now())
                    .updateTime(LocalDateTime.now())
                    .expireTime(DateUtils.localDateTimeToSecond(LocalDateTime.now()) + ExpireTimeEnum.ONE_WEEK.getTime())
                    .build();
            super.save(userToken);
            return userToken;
        } catch (Exception e) {
            throw new BaseException("创建token异常!");
        }
    }

    @Override
    public void deleteUserToken(Long userId, Integer deviceType) {
        UserToken userToken = super.getOne(new LambdaQueryWrapper<UserToken>()
                .eq(UserToken::getUserId, userId)
                .eq(UserToken::getDeviceType, deviceType)
        );
        if (userToken == null) {
            return;
        }
        //根据主键删除数据库
        super.removeById(userToken.getId());
        //删除缓存
        String userTokenKey = this.getUserTokenKey(userId, userToken.getDeviceType());
        redisUtil.deleteCache(userTokenKey);
        String tokenInfoKey = this.getTokenInfoKey(userToken.getToken(), userToken.getDeviceType());
        redisUtil.deleteCache(tokenInfoKey);
    }

    private String createToken(Long userId, Integer deviceType) {
        String random = String.join("|",
                userId.toString(),
                deviceType.toString(),
                UUID.randomUUID().toString(),
                Long.toString(System.nanoTime())
        );
        //先进行AES加密
        String encrypted = AESUtil.encrypt(random, this.tokenSecret);
        //创建token
        return Base64.getUrlEncoder().withoutPadding().encodeToString(encrypted.getBytes());
    }

    private String getUserTokenKey(Long userId, Integer deviceType) {
        return RedisKeyConstant.USER_TOKEN_KEY + "_" + userId + "_" + deviceType;
    }

    private String getTokenInfoKey(String token, Integer deviceType) {
        return RedisKeyConstant.TOKEN_INFO_KEY + "_" + token + "_" + deviceType;
    }
}
