package com.xindazhou.user.service.oauth2;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.xindazhou.framework.common.enums.UserTypeEnum;
import com.xindazhou.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.xindazhou.framework.common.util.date.DateUtils;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.xindazhou.framework.common.util.object.BeanUtils;
import com.xindazhou.framework.security.core.LoginUser;
import com.xindazhou.framework.security.core.jwt.OAuth2JwtUtil;
import com.xindazhou.framework.tenant.core.context.TenantContextHolder;
import com.xindazhou.framework.tenant.core.util.TenantUtils;
import com.xindazhou.framework.tenant.core.aop.TenantIgnore;
import com.xindazhou.user.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.xindazhou.user.dal.dataobject.oauth2.OAuth2ClientDO;
import com.xindazhou.user.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
import com.xindazhou.user.dal.dataobject.user.MemberUserDO;
import com.xindazhou.user.dal.mysql.oauth2.OAuth2AccessTokenMapper;
import com.xindazhou.user.dal.mysql.oauth2.OAuth2RefreshTokenMapper;
import com.xindazhou.user.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
import com.xindazhou.user.dal.redis.oauth2.TokenBlacklistRedisDAO;
import com.xindazhou.user.service.user.MemberUserService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.xindazhou.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static com.xindazhou.framework.common.util.collection.CollectionUtils.convertSet;

/**
 * OAuth2.0 Token Service 实现类
 *
 * @author hmy
 */
@Service
public class OAuth2TokenServiceImpl implements OAuth2TokenService {

    @Resource
    private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
    @Resource
    private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;

    @Resource
    private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO;

    @Resource
    private TokenBlacklistRedisDAO tokenBlacklistRedisDAO;

    @Resource
    private OAuth2ClientService oauth2ClientService;
    @Resource
    @Lazy // 懒加载，避免循环依赖
    private MemberUserService memberUserService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    @TenantIgnore // Token 创建时无需租户编号
    public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
        OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
        // 创建刷新令牌
        OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
        // 创建访问令牌
        return createOAuth2AccessToken(refreshTokenDO, clientDO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
        // 查询访问令牌
        OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
        if (refreshTokenDO == null) {
            throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌");
        }

        // 校验 Client 匹配
        OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
        if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) {
            throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确");
        }

        // 移除相关的访问令牌
        List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
        if (CollUtil.isNotEmpty(accessTokenDOs)) {
            oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
            oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken));
        }

        // 已过期的情况下，删除刷新令牌
        if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
            oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId());
            throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期");
        }

        // 创建访问令牌
        return createOAuth2AccessToken(refreshTokenDO, clientDO);
    }

    @Override
    public OAuth2AccessTokenDO getAccessToken(String accessToken) {
        // 优先从 Redis 中获取
        OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
        if (accessTokenDO != null) {
            return accessTokenDO;
        }

        // 获取不到，从 MySQL 中获取访问令牌
        accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            // 特殊：从 MySQL 中获取刷新令牌。原因：解决部分场景不方便刷新访问令牌场景
            // 例如说，积木报表只允许传递 token，不允许传递 refresh_token，导致无法刷新访问令牌
            // 再例如说，前端 WebSocket 的 token 直接跟在 url 上，无法传递 refresh_token
            OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(accessToken);
            if (refreshTokenDO != null && !DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
                accessTokenDO = convertToAccessToken(refreshTokenDO);
            }
        }

        // 如果在 MySQL 存在，则往 Redis 中写入
        if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
            oauth2AccessTokenRedisDAO.set(accessTokenDO);
        }
        return accessTokenDO;
    }

    @Override
    public OAuth2AccessTokenDO checkAccessToken(String accessToken) {
        OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);
        if (accessTokenDO == null) {
            throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在");
        }
        if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
            throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期");
        }
        return accessTokenDO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
        // 删除访问令牌
        OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            return null;
        }
        
        // 计算 Token 剩余过期时间（秒）
        long expireSeconds = 0;
        if (accessTokenDO.getExpiresTime() != null) {
            expireSeconds = LocalDateTimeUtil.between(LocalDateTime.now(), accessTokenDO.getExpiresTime(), ChronoUnit.SECONDS);
            if (expireSeconds < 0) {
                expireSeconds = 0; // 已过期，不需要加入黑名单
            }
        }
        
        // 1. 从数据库删除
        oauth2AccessTokenMapper.deleteById(accessTokenDO.getId());
        // 2. 从 Redis 删除
        oauth2AccessTokenRedisDAO.delete(accessToken);
        // 3. 加入黑名单（实现立即失效）
        if (expireSeconds > 0) {
            tokenBlacklistRedisDAO.addToBlacklist(accessToken, expireSeconds);
        }
        // 4. 删除刷新令牌
        oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken());
        return accessTokenDO;
    }

    @Override
    public void removeAccessToken(Long userId, Integer userType) {
        List<OAuth2AccessTokenDO> accessTokens = oauth2AccessTokenMapper.selectListByUserIdAndUserType(userId, userType);
        if (CollUtil.isEmpty(accessTokens)) {
            return;
        }
        accessTokens.forEach(accessToken -> {
            // 计算 Token 剩余过期时间（秒）
            long expireSeconds = 0;
            if (accessToken.getExpiresTime() != null) {
                expireSeconds = LocalDateTimeUtil.between(LocalDateTime.now(), accessToken.getExpiresTime(), ChronoUnit.SECONDS);
                if (expireSeconds < 0) {
                    expireSeconds = 0; // 已过期，不需要加入黑名单
                }
            }
            
            // 1. 从数据库删除
            oauth2AccessTokenMapper.deleteById(accessToken.getId());
            // 2. 从 Redis 删除
            oauth2AccessTokenRedisDAO.delete(accessToken.getAccessToken());
            // 3. 加入黑名单（实现立即失效）
            if (expireSeconds > 0) {
                tokenBlacklistRedisDAO.addToBlacklist(accessToken.getAccessToken(), expireSeconds);
            }
            // 4. 删除刷新令牌
            oauth2RefreshTokenMapper.deleteByRefreshToken(accessToken.getRefreshToken());
        });
    }

    private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
        // 优先从 refreshToken 获取租户编号，避免 ThreadLocal 被污染时导致 tenantId 为 null
        Long tenantId = refreshTokenDO.getTenantId();
        if (tenantId == null) {
            tenantId = TenantContextHolder.getTenantId();
            if (tenantId == null) {
                tenantId = 1L; // 默认租户编号
            }
        }
        
        // 计算过期时间
        long expireSeconds = clientDO.getAccessTokenValiditySeconds();
        LocalDateTime expiresTime = LocalDateTime.now().plusSeconds(expireSeconds);
        
        // 生成 JWT Token（而不是 UUID）
        String jwtToken = OAuth2JwtUtil.generateToken(
                refreshTokenDO.getUserId(),
                refreshTokenDO.getUserType(),
                tenantId,
                refreshTokenDO.getScopes(),
                expireSeconds
        );
        
        if (jwtToken == null) {
            throw new RuntimeException("生成 OAuth2 JWT Token 失败");
        }
        
        OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO();
        accessTokenDO.setAccessToken(jwtToken); // JWT Token 字符串
        accessTokenDO.setUserId(refreshTokenDO.getUserId());
        accessTokenDO.setUserType(refreshTokenDO.getUserType());
        accessTokenDO.setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType()));
        accessTokenDO.setClientId(clientDO.getClientId());
        accessTokenDO.setScopes(refreshTokenDO.getScopes());
        accessTokenDO.setRefreshToken(refreshTokenDO.getRefreshToken());
        accessTokenDO.setExpiresTime(expiresTime);
        accessTokenDO.setTenantId(tenantId);
        
        oauth2AccessTokenMapper.insert(accessTokenDO);
        // 记录到 Redis 中
        oauth2AccessTokenRedisDAO.set(accessTokenDO);
        return accessTokenDO;
    }

    private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List<String> scopes) {
        OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
                .setUserId(userId).setUserType(userType)
                .setClientId(clientDO.getClientId()).setScopes(scopes)
                .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
        // 设置租户编号（如果租户功能已禁用，使用默认值 1）
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            tenantId = 1L; // 默认租户编号
        }
        refreshToken.setTenantId(tenantId);
        oauth2RefreshTokenMapper.insert(refreshToken);
        return refreshToken;
    }

    private OAuth2AccessTokenDO convertToAccessToken(OAuth2RefreshTokenDO refreshTokenDO) {
        OAuth2AccessTokenDO accessTokenDO = BeanUtils.toBean(refreshTokenDO, OAuth2AccessTokenDO.class)
                .setAccessToken(refreshTokenDO.getRefreshToken());
        TenantUtils.execute(refreshTokenDO.getTenantId(),
                        () -> accessTokenDO.setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType())));
        return accessTokenDO;
    }

    /**
     * 加载用户信息，方便 {@link com.xindazhou.framework.security.core.LoginUser} 获取到昵称、部门等信息
     *
     * @param userId 用户编号
     * @param userType 用户类型
     * @return 用户信息
     */
    private Map<String, String> buildUserInfo(Long userId, Integer userType) {
        if (userId == null || userId <= 0) {
            return Collections.emptyMap();
        }
        if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
            // C端用户：从用户服务获取
            MemberUserDO user = memberUserService.getUser(userId);
            if (user != null) {
                return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname())
                        .build();
            }
            return Collections.emptyMap();
        } else if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
            // 管理后台用户：不在用户服务，返回空（如果需要，可以通过 Feign 调用业务服务）
            return Collections.emptyMap();
        }
        throw new IllegalArgumentException("未知用户类型：" + userType);
    }

    // 已废弃：现在使用 JWT Token，不再使用 UUID
    // private static String generateAccessToken() {
    //     return IdUtil.fastSimpleUUID();
    // }

    private static String generateRefreshToken() {
        return IdUtil.fastSimpleUUID();
    }

    @Override
    public boolean isTokenRevoked(String accessToken) {
        // 1. 优先检查黑名单（最快路径）
        if (tokenBlacklistRedisDAO.isBlacklisted(accessToken)) {
            // 在黑名单中，说明已被撤销
            return true;
        }
        
        // 2. 检查 Redis 中是否有有效 Token（快速路径）
        OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
        if (accessTokenDO != null) {
            // Redis 中存在，说明 Token 未撤销
            return false;
        }
        
        // 3. Redis 中没有，查数据库（慢速路径）
        accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            // 数据库中也找不到，说明已被撤销
            return true;
        }
        
        // 4. 数据库中存在，写入 Redis（下次直接从 Redis 获取，提升性能）
        if (!DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
            oauth2AccessTokenRedisDAO.set(accessTokenDO);
        }
        return false;
    }

}

