package com.luojia_channel.common.utils;

import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.exception.UserException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.luojia_channel.common.constants.RedisConstant.BLACKLIST_PREFIX;
import static com.luojia_channel.common.constants.RedisConstant.REFRESH_TOKEN_PREFIX;

@Slf4j
@Component
@RequiredArgsConstructor
public final class JWTUtil {

    private static final long ACCESS_EXPIRATION = 60 * 60 * 1000; //一小时
    private static final long REFRESH_EXPIRATION = 60 * 60 * 24 * 15 * 1000; //15天
    private static final long NEED_REFRESH_TTL = 60 * 60 * 24 * 7 * 1000; //7天
    private static final String USER_ID_KEY = "userId";
    private static final String USER_NAME_KEY = "username";
    private static final String USER_AVATAR_KEY = "avatar";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String ISS = "luojiachannel";
    public static final String SECRET = "SecretKey5464Created2435By54377Forely02345239354893543157956476525685754352976546564766315468763584576";

    private final RedisUtil redisUtil;

    /**
     * 生成accessToken
     * @param userInfo
     * @return
     */
    public String generateAccessToken(UserDTO userInfo) {
        Map<String, Object> customerUserMap = new HashMap<>();
        customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
        customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
        customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
        String jwtToken = Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setIssuedAt(new Date())
                .setIssuer(ISS)
                .setSubject(JSON.toJSONString(customerUserMap))
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRATION))
                .compact();
        return jwtToken;
    }

    /**
     * 生成refreshToken
     * @param userInfo
     * @return
     */
    public String generateRefreshToken(UserDTO userInfo) {
        Map<String, Object> customerUserMap = new HashMap<>();
        customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
        customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
        customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
        String jwtToken = Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setIssuedAt(new Date())
                .setIssuer(ISS)
                .setSubject(JSON.toJSONString(customerUserMap))
                .setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRATION))
                .compact();
        return jwtToken;
    }

    /**
     * 解析token获得用户
     * @param accessToken
     * @return
     */
    public UserDTO parseJwtToken(String accessToken) {
        if (!StringUtils.hasText(accessToken) || !accessToken.startsWith(TOKEN_PREFIX)) {
            throw new UserException("Token格式错误");
        }
        String rawAccessToken = accessToken.replace(TOKEN_PREFIX, "");
        try {
            Claims claims = parseToken(rawAccessToken);
            Date expiration = claims.getExpiration();
            if (expiration.after(new Date())) {
                String subject = claims.getSubject();
                return JSON.parseObject(subject, UserDTO.class);
            }
        } catch (ExpiredJwtException ignored) {
        } catch (Exception ex) {
            log.error("JWT Token解析失败，请检查", ex);
        }
        return null;
    }

    /**
     * 检查登录
     * @param accessToken
     * @param refreshToken
     * @return
     */
    public UserDTO checkLogin(String accessToken, String refreshToken) {
        if (!StringUtils.hasText(accessToken) || !accessToken.startsWith(TOKEN_PREFIX)) {
            throw new UserException("Token格式错误");
        }
        String rawAccessToken = accessToken.replace(TOKEN_PREFIX, "");
        // 在黑名单中
        if (redisUtil.hasKey(BLACKLIST_PREFIX + rawAccessToken)) {
            throw new UserException("Token已失效");
        }
        try {
            Claims claims = parseToken(rawAccessToken);
            UserDTO user = extractUserFromClaims(claims);
            if (needRefresh(claims)) {
                return refreshTokens(user, refreshToken);
            }
            return user;
        } catch (ExpiredJwtException ex) {
            // accessToken过期，尝试用refreshToken刷新
            // 注意，这里不能直接用过期时间与现在时间做差判断，因为过期会抛异常
            return handleExpiredToken(ex, refreshToken);
        } catch (Exception ex) {
            throw new UserException("Token无效");
        }
    }

    private boolean needRefresh(Claims claims) {
        long remaining = claims.getExpiration().getTime() - System.currentTimeMillis();
        return remaining < 30 * 60 * 1000; // 剩余30分钟刷新
    }

    private UserDTO refreshTokens(UserDTO user, String refreshToken) {
        String redisKey = REFRESH_TOKEN_PREFIX + user.getUserId();
        String storedRefreshToken = redisUtil.get(redisKey, String.class);
        if (!refreshToken.equals(storedRefreshToken)) {
            throw new UserException("refreshToken无效");
        }
        // 生成新token并更新redis
        String newAccessToken = generateAccessToken(user);
        String newRefreshToken = generateRefreshToken(user);
        // 惰性刷新refreshToken
        Long ttl = redisUtil.getExpire(redisKey, TimeUnit.MILLISECONDS);
        if(ttl < NEED_REFRESH_TTL)
            redisUtil.set(redisKey, newRefreshToken, REFRESH_EXPIRATION, TimeUnit.MILLISECONDS);
        user.setAccessToken(newAccessToken);

        return user;
    }

    private UserDTO handleExpiredToken(ExpiredJwtException ex, String refreshToken) {
        // 从过期token中提取用户信息
        UserDTO user = extractUserFromClaims(ex.getClaims());
        // 刷新双 Token
        return refreshTokens(user, refreshToken);
    }

    private Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }

    private UserDTO extractUserFromClaims(Claims claims) {
        String subject = claims.getSubject();
        return JSON.parseObject(subject, UserDTO.class);
    }

    public long getRemainingTime(String rawToken){
        Claims claims = parseToken(rawToken);
        return claims.getExpiration().getTime()-System.currentTimeMillis();
    }

}