package com.infoepoch.pms.agent.service.auth;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.infoepoch.pms.agent.platform.shared.exception.BusinessException;
import com.infoepoch.pms.agent.platform.shared.response.ResultCode;
import com.infoepoch.pms.agent.properties.SsoProperties;
import com.infoepoch.pms.agent.service.auth.dto.ExternalSsoUser;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;

/**
 * 外部系统 SSO Token 验证服务。
 * <p>
 * 负责解析、解密、校验 Token 的完整流程，包括时效校验和防重放保护。
 * </p>
 */
@Service
@RequiredArgsConstructor
public class ExternalSsoTokenVerifier {

    private static final int GCM_TAG_LENGTH = 128;

    private final SsoProperties ssoProperties;
    private final UsedSsoTokenService usedSsoTokenService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 验证 SSO Token 的完整流程：解析结构、获取密钥、解密 payload、校验字段、防重放。
     *
     * @param token 原始 Token 字符串
     * @return 验证通过后的用户信息
     * @throws BusinessException Token 不合法、已过期或已被使用时
     */
    public ExternalSsoUser verify(String token) {
        String[] parts = parseTokenParts(token);
        String issuer = parts[1];
        String kid = parts[2];
        String aesKey = ssoProperties.getAesKey(issuer, kid);
        if (StringUtils.isBlank(aesKey)) {
            throw fail("单点登录失败，未信任的来源系统或密钥版本：" + issuer + "/" + kid);
        }

        Map<String, Object> payload = decryptPayload(parts, issuer, kid, aesKey);
        verifyPayload(payload, issuer, kid);
        return new ExternalSsoUser(issuer, stringPayload(payload, "username"), stringPayload(payload, "jti"));
    }

    /**
     * 解析 Token 各组成部分（版本、颁发者、密钥版本、IV、密文）。
     *
     * @param token 原始 Token 字符串
     * @return 以 "." 分割的 Token 片段数组
     * @throws BusinessException Token 格式不正确时
     */
    private String[] parseTokenParts(String token) {
        if (StringUtils.isBlank(token)) {
            throw fail("单点登录失败，Token 格式错误");
        }
        String[] parts = token.split("\\.");
        if (parts.length != 5 || !"v1".equals(parts[0])) {
            throw fail("单点登录失败，Token 格式错误");
        }
        return parts;
    }

    /**
     * 使用 AES-GCM 解密 Token payload。
     *
     * @param parts  Token 片段数组
     * @param issuer 来源系统标识
     * @param kid    密钥版本标识
     * @param aesKey Base64 编码的 AES 密钥
     * @return 解密后的 payload JSON 对象
     * @throws BusinessException 解密或校验失败时
     */
    private Map<String, Object> decryptPayload(String[] parts, String issuer, String kid, String aesKey) {
        try {
            Base64.Decoder decoder = Base64.getUrlDecoder();
            byte[] iv = decoder.decode(parts[3]);
            byte[] encrypted = decoder.decode(parts[4]);

            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(decodeKey(aesKey), "AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
            cipher.updateAAD(buildAad(issuer, kid));
            byte[] plaintext = cipher.doFinal(encrypted);
            return objectMapper.readValue(plaintext, new TypeReference<>() {
            });
        } catch (AEADBadTagException ex) {
            throw fail("单点登录失败，Token 解密或校验失败");
        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw fail("单点登录失败，Token 解密或校验失败");
        }
    }

    /**
     * 校验 Token payload 各字段的合法性：颁发者、接收方、时间窗口和防重放。
     *
     * @param payload 解密后的 payload
     * @param issuer  来源系统标识
     * @param kid     密钥版本标识
     * @throws BusinessException 任一校验不通过时
     */
    private void verifyPayload(Map<String, Object> payload, String issuer, String kid) {
        if (!issuer.equals(payload.get("iss"))) {
            throw fail("单点登录失败，Token 来源系统不匹配");
        }
        if (!kid.equals(payload.get("kid"))) {
            throw fail("单点登录失败，Token 密钥版本不匹配");
        }
        if (!ssoProperties.getAudience().equals(payload.get("aud"))) {
            throw fail("单点登录失败，Token 接收方错误");
        }
        String username = stringPayload(payload, "username");
        if (StringUtils.isBlank(username)) {
            throw fail("单点登录失败，Token 缺少用户名");
        }
        String tokenId = stringPayload(payload, "jti");
        if (StringUtils.isBlank(tokenId)) {
            throw fail("单点登录失败，Token 缺少一次性标识");
        }

        long now = System.currentTimeMillis();
        long iat = longPayload(payload, "iat");
        long exp = longPayload(payload, "exp");
        long clockSkewMillis = TimeUnitSupport.secondsToMillis(ssoProperties.getClockSkewSeconds());
        long maxLifetimeMillis = TimeUnitSupport.secondsToMillis(ssoProperties.getTokenExpireSeconds());

        if (iat > now + clockSkewMillis) {
            throw fail("单点登录失败，Token 签发时间异常");
        }
        if (exp <= iat || exp - iat > maxLifetimeMillis + clockSkewMillis) {
            throw fail("单点登录失败，Token 有效期异常");
        }
        if (exp < now) {
            throw fail("单点登录失败，Token 已过期");
        }
        if (!usedSsoTokenService.markUsed(issuer, tokenId, exp)) {
            throw fail("单点登录失败，Token 已被使用");
        }
    }

    /**
     * 解码 Base64 编码的 AES 密钥并校验长度（必须为 32 字节）。
     *
     * @param value Base64 编码的密钥字符串
     * @return 解码后的密钥字节数组
     * @throws BusinessException 密钥格式或长度不正确时
     */
    private byte[] decodeKey(String value) {
        try {
            byte[] key = Base64.getDecoder().decode(value);
            if (key.length != 32) {
                throw fail("单点登录失败，AES 密钥必须是 Base64 编码的 32 字节随机值");
            }
            return key;
        } catch (IllegalArgumentException ex) {
            throw fail("单点登录失败，AES 密钥必须是 Base64 编码的 32 字节随机值");
        }
    }

    /**
     * 构造 AES-GCM 的附加认证数据（AAD），格式为 v1.{issuer}.{kid}.{audience}。
     *
     * @param issuer 来源系统标识
     * @param kid    密钥版本标识
     * @return AAD 字节数组
     */
    private byte[] buildAad(String issuer, String kid) {
        return ("v1." + issuer + "." + kid + "." + ssoProperties.getAudience())
                .getBytes(StandardCharsets.UTF_8);
    }

    /** 从 payload 中获取字符串字段值，字段不存在时返回 null。 */
    private String stringPayload(Map<String, Object> payload, String name) {
        Object value = payload.get(name);
        return value == null ? null : String.valueOf(value);
    }

    /** 从 payload 中获取长整型字段值，字段不存在或非数字时抛出异常。 */
    private long longPayload(Map<String, Object> payload, String name) {
        Object value = payload.get(name);
        if (value instanceof Number number) {
            return number.longValue();
        }
        throw fail("单点登录失败，Token 缺少时间字段");
    }

    /** 构造参数校验失败的业务异常。 */
    private BusinessException fail(String message) {
        return new BusinessException(ResultCode.PARAM_ERROR, message);
    }

    private static class TimeUnitSupport {
        private static long secondsToMillis(long seconds) {
            return seconds * 1000L;
        }
    }
}
