package com.sonic.bear.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.sonic.common.auth.domains.Session;
import com.sonic.bear.domain.entity.UserSession;
import com.sonic.bear.service.SessionCacheService;
import com.sonic.bear.service.UserSessionService;
import com.sonic.common.auth.domains.SessionStatusEnum;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Optional;
import java.util.Random;

/**
 * @author coder
 */
@Service
public class SessionCacheServiceImpl implements SessionCacheService, InitializingBean {
    /**
     * 默认redis session缓存时间 8小时
     */
    private static volatile Duration DEFAULT_REDIS_SESSION_CACHE_SECONDS = Duration.ofSeconds(8 * 60 * 60);

    /**
     * 默认local session缓存时间 2小时
     */
    private static volatile Duration DEFAULT_LOCAL_SESSION_CACHE_SECONDS = Duration.ofSeconds(2 * 60 * 60);

    private static volatile Random RANDOM_MINUTE_IN_HOURS = new Random(60);

    private LoadingCache<String, Session> sessionCache;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private UserSessionService userSessionService;

    private String sessionRedisKey(String token) {
        return String.format("bs:sso:session:%s", token);
    }

    @Override
    public Optional<Session> get(String token) {
        // 通过get获取缓存，如果没有，则异步执行load从redis加载数据。即，多线程情况，可能会加载到旧值
        return Optional.ofNullable(sessionCache.get(token));
    }

    @Override
    public void cache(String token, Session session) {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(token));
        Preconditions.checkNotNull(session);

        // 削峰，避免短时间内redis大量缓存过期，对数据库造成压力，所以redis过期时间随机分布在60分钟内
        Duration timeout = DEFAULT_REDIS_SESSION_CACHE_SECONDS.plusMinutes(RANDOM_MINUTE_IN_HOURS.nextInt(60));
        redisTemplate.opsForValue().set(sessionRedisKey(token), JSON.toJSONString(session), timeout);
        sessionCache.put(token, session);
    }

    @Override
    public void localCache(String token, Session session) {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(token));
        Preconditions.checkNotNull(session);

        sessionCache.put(token, session);
    }

    @Override
    public void invalidate(String token) {
        redisTemplate.delete(sessionRedisKey(token));
        sessionCache.invalidate(token);
    }

    @Override
    public void afterPropertiesSet() {
        sessionCache = Caffeine.newBuilder()
                .expireAfterAccess(DEFAULT_LOCAL_SESSION_CACHE_SECONDS)
                // TODO: 目前活跃用户不到1w，这里最大按50倍计算，设置50w缓存数量
                .maximumSize(500_000)
                .initialCapacity(500_000 / 50)
                .recordStats()
                .removalListener((RemovalListener<String, Session>) (key, value, cause) -> {
                    // 缓存失效时，把数据更新回db
                    if (value == null) {
                        return;
                    }
                    UserSession userSession = userSessionService.get(key).orElse(null);
                    if (userSession == null || !userSession.getToken().equals(key)) {
                        return;
                    }

                    UserSession toUpdate = UserSession.builder()
                            .expireTime(value.getExpireTime())
                            .lastAccessTime(value.getLastAccessTime())
                            .status(value.isExpired() ? SessionStatusEnum.EXPIRED : userSession.getStatus()).build();
                    userSessionService.update(toUpdate, Wrappers.<UserSession>lambdaQuery()
                            .eq(UserSession::getId, userSession.getId())
                            .eq(UserSession::getStatus, userSession.getStatus()));
                })
                .build(new CacheLoader<String, Session>() {
                    @Nullable
                    @Override
                    public Session load(@NonNull String key) {
                        return JSON.parseObject(redisTemplate.opsForValue().get(sessionRedisKey(key)), Session.class);
                    }
                });
    }
}
