package com.sonic.bear.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sonic.bear.dao.UserSessionDao;
import com.sonic.bear.domain.entity.AppClient;
import com.sonic.bear.domain.entity.UserInfoBo;
import com.sonic.bear.domain.entity.UserSession;
import com.sonic.bear.domain.input.KickOutInput;
import com.sonic.bear.domain.input.LogoutInput;
import com.sonic.bear.enums.ToastResultCode;
import com.sonic.bear.service.*;
import com.sonic.bear.utils.RedisKeyUtils;
import com.sonic.bear.utils.UUIDGenerator;
import com.sonic.common.AppRuntime;
import com.sonic.common.auth.domains.Session;
import com.sonic.common.auth.domains.SessionStatusEnum;
import com.sonic.common.utils.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author coder
 */
@Slf4j
@Service
public class UserSessionServiceImpl extends ServiceImpl<UserSessionDao, UserSession> implements UserSessionService {

    @Autowired
    private UserService userService;
    @Autowired
    private UserSessionDao userSessionDao;
    @Autowired
    private SessionCacheService sessionCacheService;
    @Autowired
    private SessionKickOutCacheService sessionKickOutCacheService;
    @Autowired
    private AppClientService appClientService;
    @Autowired
    private RedisLock.RedisWrapper redisWrapper;
    @Autowired
    private AppRuntime appRuntime;
    @Autowired
    private UserAccessLogService userAccessLogService;
    @Autowired
    private SessionExistVerifyCacheService sessionExistVerifyCacheService;
    @Autowired
    private UserCommonlyUsedLogService userCommonlyUsedLogService;
    @Autowired
    private RedisKeyUtils redisKeyUtils;

    @Transactional(rollbackFor = Throwable.class)
    @Override
    public Session createSession(CreateSessionReq param) {
        //获取用户信息，判断用户是否存在能否进行登录操作
        UserInfoBo userInfoBo = userService.queryUserInfo(param.getUserId());
        ToastResultCode.USER_NOT_EXIST.check(userInfoBo == null);
        //创建登录授权凭证
        return createSession(param, userInfoBo);
    }


    /**
     * 将历史登录凭证进行失效
     */
    private void expireSessionWhenMobileDeviceOnLine(Long userId) {
        LambdaQueryWrapper<UserSession> queryWrapper = Wrappers.<UserSession>lambdaQuery()
                .eq(UserSession::getStatus, SessionStatusEnum.ENABLED)
                .eq(UserSession::getUserId, userId);
        UserSession userSession = userSessionDao.selectOne(queryWrapper);
        if (userSession != null) {
            sessionCacheService.invalidate(userSession.getToken());
            userSessionDao.expiredSessionV2(userId);
            //将无效的token添加到kickOut中 用户进行数据排除
            sessionKickOutCacheService.cache(userSession.getToken());
        }
    }

    private Session insertSession(CreateSessionReq param, AppClient appClient, UserInfoBo userInfoBo, LocalDateTime now) {
        UserSession userSession = UserSession.builder()
                .userId(param.getUserId())
                .accountType(userInfoBo.getAccountTypeEnum())
                .token(UUIDGenerator.generate32Uuid())
                .clientCode(param.getClientCode())
                .loginType(param.getLoginType())
                .status(SessionStatusEnum.ENABLED)
                .userAgent(param.getUserAgent())
                .deviceId(param.getDeviceId())
                .ip(param.getIp())
                .createTime(now)
                .lastAccessTime(now)
                .expireTime(generateExpiredTime(appClient.getSessionExpired(), appClient.getSessionAlive(), now))
                .build();
        baseMapper.insert(userSession);

        //添加用户常用设备日志数据 IP地址 设备ID
        userCommonlyUsedLogService.addData(param.getUserId(), 1, param.getIp());
        userCommonlyUsedLogService.addData(param.getUserId(), 2, param.getDeviceId());

        Session session = convertToSession(userSession);
        session.setEndpoint(userSession.getClientCode());
        sessionCacheService.cache(session.getToken(), session);
        //将缓存数据写入是否存在的验证缓存中以防止穿透数据库
        sessionExistVerifyCacheService.cache(session.getToken(), appClient.getSessionExpired().longValue() * 60);
        return session;
    }


    private void updateInvalidSession(CreateSessionReq param, LocalDateTime now) {
        String clientCode = param.getClientCode();
        Long userId = param.getUserId();
        List<UserSession> userSessionList = baseMapper.selectList(
                Wrappers.lambdaQuery(UserSession.builder()
                        .userId(userId)
                        .clientCode(clientCode)
                        .status(SessionStatusEnum.ENABLED).build()));
        if (CollectionUtils.isEmpty(userSessionList)) {
            return;
        }
        for (UserSession userSession : userSessionList) {
            sessionCacheService.invalidate(userSession.getToken());
            if (userSession.isExpired()) {
                userSession.setStatus(SessionStatusEnum.EXPIRED);
            } else {
                userSession.setStatus(SessionStatusEnum.LOGOUT).setExpireTime(now);
            }
            update(userSession, Wrappers.<UserSession>lambdaQuery()
                    .eq(UserSession::getId, userSession.getId())
                    .eq(UserSession::getStatus, SessionStatusEnum.ENABLED));
            //将无效的token添加到kickOut中 用户进行数据排除
            sessionKickOutCacheService.cache(userSession.getToken());
            //将已经存在的token删除掉
            sessionExistVerifyCacheService.clearCache(userSession.getToken());
        }
    }


    @Override
    public Session touchSession(String token) {
        LocalDateTime now = LocalDateTime.now();
        Optional<Session> sessionOpt = extendCacheSessionValidity(token, now);
        if (sessionOpt.isPresent()) {
            Session session = sessionOpt.get();
            //判断大小写必须完全保持一致（防止数据库忽略大小写的问题发生）
            ToastResultCode.SESSION_INVALID.check(!token.equals(session.getToken()));
            //写入访问日志数据到记录表中
            userAccessLogService.addLog(session.getUserId(), session.getEndpoint());
            return session;
        }

        //查询数据库验证token的有效性
        Optional<UserSession> userSessionOptional = get(token);
        ToastResultCode.SESSION_INVALID.check(!userSessionOptional.isPresent());
        UserSession userSession = userSessionOptional.get();
        ToastResultCode.SESSION_INVALID.check(!userSession.isEnabled());
        //判断大小写的问题
        ToastResultCode.SESSION_INVALID.check(!token.equals(userSession.getToken()));

        //写入访问日志数据到记录表中
        userAccessLogService.addLog(userSession.getUserId(), userSession.getClientCode());

        Optional<AppClient> appClientOptional = appClientService.getByCode(userSession.getClientCode());
        ToastResultCode.APP_CLIENT_NOT_EXIST.check(!appClientOptional.isPresent());
        AppClient appClient = appClientOptional.get();
        // 如果已经过期，更新session状态，并直接抛出异常提示
        if (userSession.isExpired() || userSession.isOverMaxAlive(appClient.getSessionAlive())) {
            baseMapper.updateById(UserSession.builder()
                    .id(userSession.getId())
                    .status(SessionStatusEnum.EXPIRED)
                    .lastAccessTime(LocalDateTime.now()).build());
            //TODO 清理掉缓存数据
            sessionExistVerifyCacheService.clearCache(userSession.getToken());
            ToastResultCode.SESSION_EXPIRED.check(true);
        }

        //过期时间 之前是用当前时间来更新的，现在换成创建时间来进行判断
        LocalDateTime expireTime = generateExpiredTime(appClient.getSessionExpired(), appClient.getSessionAlive(), userSession.getCreateTime());
        // 延期session
        baseMapper.updateById(UserSession.builder()
                .id(userSession.getId())
                .expireTime(expireTime)
                .lastAccessTime(now).build());
        userSession.setLastAccessTime(now);
        userSession.setExpireTime(expireTime);
        Session session = convertToSession(userSession);
        session.setToken(token);
        sessionCacheService.cache(token, session);
        return session;
    }

    private Optional<Session> extendCacheSessionValidity(String token, LocalDateTime now) {
        Optional<Session> sessionOpt = sessionCacheService.get(token);
        if (!sessionOpt.isPresent()) {
            return Optional.empty();
        }
        Session session = sessionOpt.get();
        Optional<AppClient> appClientOptional = appClientService.getByCode(session.getEndpoint());
        ToastResultCode.APP_CLIENT_NOT_EXIST.check(!appClientOptional.isPresent());
        AppClient appClient = appClientOptional.get();
        //校验用户是否 已经被kickOut了，如果被踢下线了则直接返回异常
        Optional<String> sessionKickOutOpt = sessionKickOutCacheService.get(token);
        if (session.isExpired() || session.isDisabled() || session.isOverMaxAlive(appClient.getSessionAlive()) || sessionKickOutOpt.isPresent()) {
            sessionCacheService.invalidate(token);
            //清理掉缓存数据
            sessionExistVerifyCacheService.clearCache(session.getToken());
            ToastResultCode.SESSION_EXPIRED.check(true);
        }
        session.setLastAccessTime(now);
        session.setExpireTime(generateExpiredTime(appClient.getSessionExpired(), appClient.getSessionAlive(), session.getCreateTime()));
        sessionCacheService.localCache(token, session);
        return sessionOpt;
    }

    private Session createSession(CreateSessionReq param, UserInfoBo userInfoBo) {
        LocalDateTime now = LocalDateTime.now();
        Optional<AppClient> clientOpt = appClientService.getByCode(param.getClientCode());
        ToastResultCode.APP_CLIENT_NOT_EXIST.check(!clientOpt.isPresent());
        AppClient appClient = clientOpt.get();
        ToastResultCode.APP_CLIENT_NOT_ALLOW.check(!appClient.allowUser(userInfoBo.getAccountTypeEnum()));

        // 用户登陆创建 session 加锁
        RedisLock redisLock = new RedisLock(redisKeyUtils.sessionCreateLockKey(userInfoBo.getUserId()), redisWrapper);
        return redisLock.tryAcquireRun(() -> {
            // 更新无效的session
            updateInvalidSession(param, now);
            // 先让已经登陆了的设备 失效
            expireSessionWhenMobileDeviceOnLine(param.getUserId());
            // 插入新的有效session
            return insertSession(param, appClient, userInfoBo, now);
        });
    }

    @Transactional(rollbackFor = Throwable.class)
    @Override
    public void logout(LogoutInput input) {
        UserSession session = get(input.getToken()).orElse(null);
        if (session == null || session.isDisabled()) {
            return;
        }
        this.updateById(UserSession.builder()
                .id(session.getId())
                .status(SessionStatusEnum.LOGOUT)
                .lastAccessTime(LocalDateTime.now())
                .build());
        sessionCacheService.invalidate(session.getToken());
        //将用户的token放入kickOut中
        sessionKickOutCacheService.cache(session.getToken());
        //清理掉缓存数据
        sessionExistVerifyCacheService.clearCache(session.getToken());
    }

    @Override
    public void kickOut(KickOutInput input) {
        List<UserSession> sessions = this.list(Wrappers.<UserSession>lambdaQuery()
                .eq(UserSession::getUserId, input.getUserId())
                .eq(UserSession::getStatus, SessionStatusEnum.ENABLED));
        if (sessions.isEmpty()) {
            return;
        }
        List<UserSession> toUpdateEnableSessions = sessions.stream().filter(UserSession::isEnabled).map(session -> {
            //将被踢出去的用户token存放到kickOut的缓存中
            sessionKickOutCacheService.cache(session.getToken());
            //清理掉缓存数据
            sessionExistVerifyCacheService.clearCache(session.getToken());
            sessionCacheService.invalidate(session.getToken());
            return UserSession.builder()
                    .id(session.getId())
                    .status(SessionStatusEnum.KICK_OUT)
                    .lastAccessTime(LocalDateTime.now())
                    .build();
        }).collect(Collectors.toList());
        if (toUpdateEnableSessions.isEmpty()) {
            return;
        }
        this.updateBatchById(toUpdateEnableSessions);
    }

    @Override
    public Optional<UserSession> get(String token) {
        LambdaQueryWrapper<UserSession> wrapper = Wrappers.lambdaQuery(UserSession.builder()
                .token(token).build());
        return Optional.ofNullable(getOne(wrapper));
    }

    /**
     * 过期时间不能超过 client 配置的最大存活时间
     *
     * @param sessionExpired session多久过期，单位分钟
     * @param sessionAlive   session存活时间，单位分钟
     * @param createTime     创建时间
     * @return 有效的过期时间
     */
    private LocalDateTime generateExpiredTime(Integer sessionExpired, Integer sessionAlive, LocalDateTime createTime) {
        long maxAliveTimeMilli = createTime.plusMinutes(sessionAlive).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        long expiredTimeMilli = LocalDateTime.now().plusMinutes(sessionExpired).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        long maxAlive = Math.min(expiredTimeMilli, maxAliveTimeMilli);
        return Instant.ofEpochMilli(maxAlive).atZone(ZoneId.systemDefault()).toLocalDateTime();
    }

    /**
     * 数据对象转换
     * @param userSession
     * @return
     */
    Session convertToSession(UserSession userSession) {
        Session session = new Session();
        session.setEndpoint(userSession.getClientCode());
        BeanUtils.copyProperties(userSession, session, "extra");
        return session;
    }

}
