package com.sonic.bear.service.impl;

import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.sonic.bear.service.SessionExistVerifyCacheService;
import lombok.extern.slf4j.Slf4j;
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.concurrent.TimeUnit;

/**
 * @Author code
 * @Description 用户Token存在性验证缓存，防止穿透
 * @Date 2022/11/14 19:03
 * @Version 1.0
 */
@Slf4j
@Service
public class SessionExistVerifyCacheServiceImpl implements SessionExistVerifyCacheService, InitializingBean {

    /**
     * 默认local exist token缓存时间 2分钟
     */
    private static volatile Duration DEFAULT_LOCAL_TOKEN_CACHE_SECONDS = Duration.ofSeconds(2 * 60);

    /**
     * 存在的token缓存
     */
    private LoadingCache<String, String> sessionExistCache;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 存在的token的redis的缓存key
     * @param token
     * @return
     */
    private String sessionExistRedisKey(String token) {
        return String.format("bs:sso:exist:%s", token);
    }

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

    @Override
    public boolean exist(String token) {
        //将异常cache住，并返回true，继续往下执行
        Optional<String> value = get(token);
        return value.isPresent();
    }

    @Override
    public void cache(String token, Long expTime) {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(token));
        // 缓存指定的时间
        redisTemplate.opsForValue().set(sessionExistRedisKey(token), "1", expTime, TimeUnit.SECONDS);
        //将缓存写入本地
        sessionExistCache.put(token, "1");
    }

    @Override
    public void initCache(String token, Long expTime) {
        // 缓存指定的时间
        redisTemplate.opsForValue().set(sessionExistRedisKey(token), "1", expTime, TimeUnit.SECONDS);
    }

    @Override
    public void clearCache(String token) {
        //清理掉本地缓存
        sessionExistCache.invalidate(token);
        //删除掉redis的缓存
        redisTemplate.delete(sessionExistRedisKey(token));
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        sessionExistCache = Caffeine.newBuilder()
                .expireAfterAccess(DEFAULT_LOCAL_TOKEN_CACHE_SECONDS)
                //TODO: 统计了30天的活跃token数据量为30万，这儿先设置最大为30万，大概会占用60M左右的内存【这儿还可以放小一点，因为这儿只会存储存在的在本地缓存中】
                .maximumSize(300_000)
                .initialCapacity(300_000 / 30)
                .recordStats()
                .build(new CacheLoader<String, String>() {
                    @Nullable
                    @Override
                    public String load(@NonNull String key) {
                        return redisTemplate.opsForValue().get(sessionExistRedisKey(key));
                    }
                });
    }
}
