package com.seer.teach.common.cache;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import jakarta.annotation.Resource;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * SSE 生命周期缓存管理类，提供两级缓存（L1 本地缓存 + L2 Redis 缓存）支持。
 * L1 使用 Caffeine 实现，分为正向缓存和负向缓存；L2 使用 Redis。
 */
@Slf4j
@Component
public class SseLifeCycleCache {

    /** 一级缓存（L1）过期时间：30分钟 */
    private static final Duration L1_TTL = Duration.ofMinutes(30);

    /** 二级缓存（L2 Redis）过期时间：30分钟 */
    private static final Duration L2_TTL = Duration.ofMinutes(30);

    /** 负向缓存过期时间：30秒，用于防止缓存穿透 */
    private static final Duration L1_NEGATIVE_TTL = Duration.ofSeconds(30);

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /** 一级正向缓存（本地） */
    private final Cache<String, Integer> l1;

    /** 一级负向缓存（本地，用于记录空值防止穿透） */
    private final Cache<String, Boolean> l1Negative;

    /**
     * 构造函数，初始化本地缓存实例。
     *
     * @param redisTemplate Redis 操作模板
     */
    public SseLifeCycleCache(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;

        // 初始化 L1 正向缓存：最大容量 50000，写入后 30 分钟过期
        this.l1 = Caffeine.newBuilder()
                .maximumSize(50_000)
                .expireAfterWrite(L1_TTL)
                .recordStats()
                .build();

        // 初始化 L1 负向缓存：最大容量 100000，写入后 30 秒过期
        this.l1Negative = Caffeine.newBuilder()
                .maximumSize(100_000)
                .expireAfterWrite(L1_NEGATIVE_TTL)
                .build();
    }

    /**
     * 获取缓存值，优先从 L1 缓存获取，未命中则查询 Redis。
     *
     * @param prefix 缓存键前缀
     * @param userId 用户 ID
     * @return 缓存值，若不存在或已失效则返回 null
     */
    public Integer get(String prefix, Integer userId) {
        String key = redisKey(prefix, userId);

        // 如果在负向缓存中存在，则直接返回 null
        if (Boolean.TRUE.equals(l1Negative.getIfPresent(key))) {
            return null;
        }

        // 尝试从 L1 正向缓存获取
        Integer local = l1.getIfPresent(key);
        if (local != null) {
            log.debug("[SseLifeCycleCache] 命中 L1, prefix={}, userId={}, value={}", prefix, userId, local);
            return local;
        }

        // L1 未命中，尝试从 Redis 获取
        Integer fromRedis = loadFromRedis(key);
        if (fromRedis != null) {
            log.debug("[SseLifeCycleCache] 命中 L2, prefix={}, userId={}, value={}", prefix, userId, fromRedis);
            l1.put(key, fromRedis);
            return fromRedis;
        }

        // Redis 也未命中，标记为负向缓存
        l1Negative.put(key, Boolean.TRUE);
        return null;
    }

    /**
     * 写入缓存值到 L1 和 Redis。
     *
     * @param prefix 缓存键前缀
     * @param userId 用户 ID
     * @param value  要写入的值，若为 null 则表示删除该缓存项
     */
    public void put(String prefix, Integer userId, Integer value) {
        String key = redisKey(prefix, userId);
        // 清除负向缓存中的记录
        l1Negative.invalidate(key);

        // 写入 Redis
        writeToRedis(key, value, L2_TTL);

        if (value != null) {
            // 值有效时写入 L1 正向缓存
            l1.put(key, value);
        } else {
            // 值无效时清除 L1 正向缓存并标记负向缓存
            l1.invalidate(key);
            l1Negative.put(key, Boolean.TRUE);
        }
    }

    /**
     * 删除指定缓存项，包括 L1 和 Redis 中的数据。
     *
     * @param prefix 缓存键前缀
     * @param userId 用户 ID
     */
    public void invalidate(String prefix, Integer userId) {
        String key = redisKey(prefix, userId);
        // 清除 L1 正向和负向缓存
        l1.invalidate(key);
        l1Negative.invalidate(key);
        // 删除 Redis 中的缓存项
        redisTemplate.delete(key);
    }

    /**
     * 直接从 Redis 中读取缓存值，不经过 L1 缓存。
     *
     * @param prefix 缓存键前缀
     * @param userId 用户 ID
     * @return Redis 中的缓存值，若不存在则返回 null
     */
    public Integer peekL2(String prefix, Integer userId) {
        return loadFromRedis(redisKey(prefix, userId));
    }

    /**
     * 直接从 L1 缓存中读取缓存值，不经过 Redis。
     *
     * @param prefix 缓存键前缀
     * @param userId 用户 ID
     * @return L1 缓存中的值，若不存在则返回 null
     */
    public Integer peekL1(String prefix, Integer userId) {
        return l1.getIfPresent(redisKey(prefix, userId));
    }

    /**
     * 从 Redis 中加载指定键的值。
     *
     * @param key Redis 键
     * @return 若值为 Integer 类型则返回，否则返回 null
     */
    private Integer loadFromRedis(String key) {
        Object raw = redisTemplate.opsForValue().get(key);
        if (raw instanceof Integer) {
            return (Integer) raw;
        }
        return null;
    }

    /**
     * 向 Redis 写入指定键值对，并设置过期时间。
     *
     * @param key   Redis 键
     * @param value 要写入的值
     * @param ttl   过期时间，若为 null 则使用默认 L2_TTL
     */
    private void writeToRedis(String key, Integer value, Duration ttl) {
        long millis = Optional.ofNullable(ttl).orElse(L2_TTL).toMillis();
        redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MILLISECONDS);
    }

    /**
     * 构造 Redis 缓存键。
     *
     * @param prefix 缓存键前缀
     * @param userId 用户 ID
     * @return 完整的 Redis 键
     */
    private String redisKey(String prefix, Integer userId) {
        return prefix + userId;
    }
}
