package com.ttstd.signaling.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 轻量 JWT（HS256）签发与校验服务。
 *
 * <p>不引入额外 JWT 库，直接基于 JDK 的 HMAC-SHA256 实现，减少依赖面。
 * 签名比对使用 {@link MessageDigest#isEqual} 以规避时序侧信道。
 */
@Service
public class JwtService {

    private static final Logger logger = LoggerFactory.getLogger(JwtService.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final Base64.Encoder B64URL = Base64.getUrlEncoder().withoutPadding();
    private static final Base64.Decoder B64URL_DEC = Base64.getUrlDecoder();

    /** 令牌用途，防止 access/refresh/device 令牌互相冒用 */
    public static final String PURPOSE_ACCESS = "access";
    public static final String PURPOSE_REFRESH = "refresh";
    public static final String PURPOSE_DEVICE = "device";

    private final SecurityProperties properties;
    private final byte[] signingKey;

    public JwtService(SecurityProperties properties) {
        this.properties = properties;
        String configured = properties.getJwt().getSecret();
        if (configured == null || configured.isBlank()) {
            byte[] random = new byte[48];
            new SecureRandom().nextBytes(random);
            this.signingKey = random;
            logger.warn("未配置 security.jwt.secret，已生成随机密钥。服务重启后所有令牌将失效，"
                    + "且多实例部署无法互认，生产环境请通过环境变量 JWT_SECRET 配置固定密钥。");
        } else if (configured.getBytes(StandardCharsets.UTF_8).length < 32) {
            throw new IllegalStateException("security.jwt.secret 长度不足，至少需要 32 字节");
        } else {
            this.signingKey = configured.getBytes(StandardCharsets.UTF_8);
        }
    }

    /**
     * 签发令牌。
     *
     * @param subject      主体 ID
     * @param purpose      令牌用途
     * @param ttlSeconds   有效期（秒）
     * @param tokenVersion 凭据版本号，与账号当前版本不一致即视为失效
     * @param extraClaims  附加声明
     */
    public String issue(String subject,
                        String purpose,
                        long ttlSeconds,
                        long tokenVersion,
                        Map<String, Object> extraClaims) {
        Instant now = Instant.now();
        Map<String, Object> header = new LinkedHashMap<>();
        header.put("alg", "HS256");
        header.put("typ", "JWT");

        Map<String, Object> claims = new LinkedHashMap<>();
        claims.put("iss", properties.getJwt().getIssuer());
        claims.put("sub", subject);
        claims.put("iat", now.getEpochSecond());
        claims.put("exp", now.plusSeconds(ttlSeconds).getEpochSecond());
        claims.put("jti", randomId());
        claims.put("pur", purpose);
        claims.put("ver", tokenVersion);
        if (extraClaims != null) {
            claims.putAll(extraClaims);
        }

        try {
            String headerPart = B64URL.encodeToString(MAPPER.writeValueAsBytes(header));
            String payloadPart = B64URL.encodeToString(MAPPER.writeValueAsBytes(claims));
            String signingInput = headerPart + "." + payloadPart;
            String signature = B64URL.encodeToString(hmacSha256(signingInput));
            return signingInput + "." + signature;
        } catch (Exception e) {
            throw new IllegalStateException("签发令牌失败", e);
        }
    }

    /**
     * 校验令牌签名、有效期与用途，返回声明集合。
     *
     * @throws AuthException 校验失败
     */
    public Map<String, Object> verify(String token, String expectedPurpose) {
        if (token == null || token.isBlank()) {
            throw AuthException.unauthorized("令牌为空");
        }
        String[] parts = token.split("\\.");
        if (parts.length != 3) {
            throw AuthException.unauthorized("令牌格式非法");
        }

        String signingInput = parts[0] + "." + parts[1];
        byte[] expected = hmacSha256(signingInput);
        byte[] actual;
        try {
            actual = B64URL_DEC.decode(parts[2]);
        } catch (IllegalArgumentException e) {
            throw AuthException.unauthorized("令牌签名编码非法");
        }
        // 常量时间比较，避免时序侧信道
        if (!MessageDigest.isEqual(expected, actual)) {
            throw AuthException.unauthorized("令牌签名无效");
        }

        Map<String, Object> claims;
        try {
            byte[] payload = B64URL_DEC.decode(parts[1]);
            @SuppressWarnings("unchecked")
            Map<String, Object> parsed = MAPPER.readValue(payload, Map.class);
            claims = parsed;
        } catch (Exception e) {
            throw AuthException.unauthorized("令牌载荷解析失败");
        }

        long skew = properties.getJwt().getClockSkewSeconds();
        long now = Instant.now().getEpochSecond();

        Object exp = claims.get("exp");
        if (!(exp instanceof Number expNum) || now > expNum.longValue() + skew) {
            throw AuthException.unauthorized("令牌已过期");
        }
        Object iat = claims.get("iat");
        if (iat instanceof Number iatNum && now + skew < iatNum.longValue()) {
            throw AuthException.unauthorized("令牌签发时间异常");
        }
        if (!properties.getJwt().getIssuer().equals(claims.get("iss"))) {
            throw AuthException.unauthorized("令牌签发方不匹配");
        }
        if (expectedPurpose != null && !expectedPurpose.equals(claims.get("pur"))) {
            throw AuthException.unauthorized("令牌用途不匹配");
        }
        return claims;
    }

    public static String claimAsString(Map<String, Object> claims, String key) {
        Object v = claims.get(key);
        return v == null ? null : String.valueOf(v);
    }

    public static long claimAsLong(Map<String, Object> claims, String key, long defaultValue) {
        Object v = claims.get(key);
        return v instanceof Number n ? n.longValue() : defaultValue;
    }

    public Map<String, Object> newClaims() {
        return new HashMap<>();
    }

    private byte[] hmacSha256(String data) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(signingKey, "HmacSHA256"));
            return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            throw new IllegalStateException("HMAC 计算失败", e);
        }
    }

    private static String randomId() {
        byte[] buf = new byte[16];
        new SecureRandom().nextBytes(buf);
        return B64URL.encodeToString(buf);
    }
}
