package cn.meowrain.aioj.backend.auth.oauth2.service;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants;
import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * OAuth2 会话管理服务 负责会话管理和 Token 黑名单（用于单点登出）
 *
 * @author meowrain
 * @since 2025-12-14
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OAuth2SessionService {

	private final StringRedisTemplate redisTemplate;

	private final JwtUtil jwtUtil;

	/**
	 * 会话有效期（秒，7天）
	 */
	private static final long SESSION_TTL = 604800L;

	/**
	 * 创建会话
	 * @param userId 用户ID
	 * @param clientId 客户端ID
	 * @param accessToken 访问令牌
	 * @param refreshToken 刷新令牌
	 * @return 会话ID
	 */
	public String createSession(Long userId, String clientId, String accessToken, String refreshToken) {
		// 1. 生成会话ID
		String sessionId = RandomUtil.randomString(32);

		// 2. 构造会话数据
		Map<String, Object> sessionData = new HashMap<>();
		sessionData.put("sessionId", sessionId);
		sessionData.put("userId", userId);
		sessionData.put("createdAt", System.currentTimeMillis());

		// 3. 构造客户端Token信息
		Map<String, String> clientTokens = new HashMap<>();
		clientTokens.put("clientId", clientId);
		clientTokens.put("accessToken", accessToken);
		clientTokens.put("refreshToken", refreshToken);
		clientTokens.put("issuedAt", String.valueOf(System.currentTimeMillis()));

		// 将客户端Token列表添加到会话
		List<Map<String, String>> clients = new ArrayList<>();
		clients.add(clientTokens);
		sessionData.put("clients", clients);

		// 4. 存储会话到 Redis
		String sessionKey = String.format(RedisKeyConstants.OAUTH2_SESSION_PREFIX, sessionId);
		redisTemplate.opsForValue().set(sessionKey, JSONUtil.toJsonStr(sessionData), SESSION_TTL, TimeUnit.SECONDS);

		// 5. 维护用户->会话映射（用于查询用户的所有会话）
		String userSessionsKey = String.format(RedisKeyConstants.OAUTH2_USER_SESSIONS_PREFIX, userId);
		redisTemplate.opsForSet().add(userSessionsKey, sessionId);
		redisTemplate.expire(userSessionsKey, SESSION_TTL, TimeUnit.SECONDS);

		log.info("创建会话: sessionId={}, userId={}, clientId={}", sessionId, userId, clientId);

		return sessionId;
	}

	/**
	 * 添加客户端到现有会话
	 * @param sessionId 会话ID
	 * @param clientId 客户端ID
	 * @param accessToken 访问令牌
	 * @param refreshToken 刷新令牌
	 */
	public void addClientToSession(String sessionId, String clientId, String accessToken, String refreshToken) {
		String sessionKey = String.format(RedisKeyConstants.OAUTH2_SESSION_PREFIX, sessionId);
		String sessionDataStr = redisTemplate.opsForValue().get(sessionKey);

		if (sessionDataStr == null) {
			log.warn("会话不存在: {}", sessionId);
			return;
		}

		// 解析会话数据
		Map<String, Object> sessionData = JSONUtil.toBean(sessionDataStr, Map.class);
		@SuppressWarnings("unchecked")
		List<Map<String, String>> clients = (List<Map<String, String>>) sessionData.get("clients");

		if (clients == null) {
			clients = new ArrayList<>();
		}

		// 添加新的客户端Token
		Map<String, String> clientTokens = new HashMap<>();
		clientTokens.put("clientId", clientId);
		clientTokens.put("accessToken", accessToken);
		clientTokens.put("refreshToken", refreshToken);
		clientTokens.put("issuedAt", String.valueOf(System.currentTimeMillis()));

		clients.add(clientTokens);
		sessionData.put("clients", clients);

		// 更新会话
		redisTemplate.opsForValue().set(sessionKey, JSONUtil.toJsonStr(sessionData), SESSION_TTL, TimeUnit.SECONDS);

		log.info("添加客户端到会话: sessionId={}, clientId={}", sessionId, clientId);
	}

	/**
	 * 撤销用户的所有会话（单点登出）
	 * @param userId 用户ID
	 */
	public void revokeAllUserSessions(Long userId) {
		log.info("撤销用户所有会话: userId={}", userId);

		// 1. 获取用户的所有会话ID
		String userSessionsKey = String.format(RedisKeyConstants.OAUTH2_USER_SESSIONS_PREFIX, userId);
		Set<String> sessionIds = redisTemplate.opsForSet().members(userSessionsKey);

		if (sessionIds == null || sessionIds.isEmpty()) {
			log.info("用户没有活跃会话: userId={}", userId);
			return;
		}

		int revokedTokenCount = 0;

		// 2. 遍历每个会话，提取所有Token并加入黑名单
		for (String sessionId : sessionIds) {
			String sessionKey = String.format(RedisKeyConstants.OAUTH2_SESSION_PREFIX, sessionId);
			String sessionDataStr = redisTemplate.opsForValue().get(sessionKey);

			if (sessionDataStr != null) {
				Map<String, Object> sessionData = JSONUtil.toBean(sessionDataStr, Map.class);
				@SuppressWarnings("unchecked")
				List<Map<String, String>> clients = (List<Map<String, String>>) sessionData.get("clients");

				if (clients != null) {
					for (Map<String, String> client : clients) {
						String accessToken = client.get("accessToken");
						String refreshToken = client.get("refreshToken");

						// 将Token加入黑名单
						if (accessToken != null) {
							blacklistToken(accessToken);
							revokedTokenCount++;
						}
						if (refreshToken != null) {
							blacklistToken(refreshToken);
							revokedTokenCount++;
						}
					}
				}

				// 删除会话
				redisTemplate.delete(sessionKey);
			}
		}

		// 3. 清空用户会话映射
		redisTemplate.delete(userSessionsKey);

		// 4. 删除用户的 Refresh Token
		String refreshTokenKey = String.format(RedisKeyConstants.REFRESH_TOKEN_KEY_PREFIX, userId);
		redisTemplate.delete(refreshTokenKey);

		log.info("撤销用户会话完成: userId={}, sessionCount={}, tokenCount={}", userId, sessionIds.size(), revokedTokenCount);
	}

	/**
	 * 检查 Token 是否在黑名单中
	 * @param token Token
	 * @return 是否在黑名单
	 */
	public boolean isTokenBlacklisted(String token) {
		String tokenHash = DigestUtil.sha256Hex(token);
		String key = String.format(RedisKeyConstants.OAUTH2_TOKEN_BLACKLIST_PREFIX, tokenHash);
		return Boolean.TRUE.equals(redisTemplate.hasKey(key));
	}

	/**
	 * 将 Token 加入黑名单
	 * @param token Token
	 */
	private void blacklistToken(String token) {
		try {
			// 1. 计算Token的SHA256哈希
			String tokenHash = DigestUtil.sha256Hex(token);
			String key = String.format(RedisKeyConstants.OAUTH2_TOKEN_BLACKLIST_PREFIX, tokenHash);

			// 2. 解析Token获取过期时间
			Claims claims = jwtUtil.parseClaims(token);
			long expiresAt = claims.getExpiration().getTime();
			long now = System.currentTimeMillis();

			// 3. 计算Token剩余有效期
			long ttl = (expiresAt - now) / 1000;

			if (ttl > 0) {
				// 4. 加入黑名单，TTL设置为Token的剩余有效期
				redisTemplate.opsForValue().set(key, "1", ttl, TimeUnit.SECONDS);
				log.debug("Token加入黑名单: tokenHash={}, ttl={}秒", tokenHash, ttl);
			}
			else {
				log.debug("Token已过期，无需加入黑名单: tokenHash={}", tokenHash);
			}
		}
		catch (Exception e) {
			log.error("Token加入黑名单失败", e);
		}
	}

}
