package com.ruoyi.app.utils.oauth;

import com.alibaba.fastjson2.JSONObject;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.NameValuePair;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 三方登录凭证校验（017-oauth-login）。
 * <p>Apple/Google/LINE 各自校验前端传来的凭证，返回稳定的 providerUid（用于绑定反查）。</p>
 * <ul>
 *   <li>Apple：nimbus 验 ES256 identityToken + 校 iss/aud/exp → sub（忽略 email）</li>
 *   <li>Google：tokeninfo HTTP 验真 + 校 audience → sub</li>
 *   <li>LINE：code 换 access_token → v2/profile → userId</li>
 * </ul>
 * <p>注：Google 走 tokeninfo 而非 firebase-admin，避免 FirebaseApp+服务账号初始化；各家真实 token + clientId 需联调验证。</p>
 *
 * @author foodie
 * @date 2026-07-30
 */
@Service
public class OAuthVerifyService {

    private static final Logger log = LoggerFactory.getLogger(OAuthVerifyService.class);

    @Value("${oauth.apple.client-id}")
    private String appleClientId;
    @Value("${oauth.apple.jwks-url}")
    private String appleJwksUrl;
    @Value("${oauth.google.client-id}")
    private String googleClientId;
    @Value("${oauth.google.tokeninfo-url}")
    private String googleTokeninfoUrl;
    @Value("${oauth.line.profile-url}")
    private String lineProfileUrl;
    @Value("${oauth.line.token-url}")
    private String lineTokenUrl;
    @Value("${oauth.line.client-id}")
    private String lineClientId;
    @Value("${oauth.line.client-secret}")
    private String lineClientSecret;
    @Value("${oauth.line.redirect-uri}")
    private String lineRedirectUri;

    /**
     * 校验 provider 凭证，返回稳定的 providerUid。
     *
     * @param provider   apple/google/line
     * @param credential Apple identityToken / Google idToken / LINE authorization code
     */
    public String verify(String provider, String credential) {
        if (provider == null || provider.isEmpty()) {
            throw new ServiceException(MessageUtils.message("no.oauth.provider.blank"));
        }
        if (credential == null || credential.isEmpty()) {
            throw new ServiceException(MessageUtils.message("no.oauth.credential.blank"));
        }
        log.info("[OAuth] 校验凭证 provider={}", provider);
        log.debug("[OAuth] {} credential={}", provider, credential);
        switch (provider) {
            case "apple":
                return verifyApple(credential);
            case "google":
                return verifyGoogle(credential);
            case "line":
                return verifyLine(credential);
            default:
                throw new ServiceException(MessageUtils.message("no.oauth.provider.unsupported", provider));
        }
    }

    /** Apple：验 ES256 identityToken，返回 sub（不使用邮箱信息）。 */
    private String verifyApple(String idToken) {
        try {
            log.debug("[OAuth][Apple] identityToken={}", idToken);
            SignedJWT jwt = SignedJWT.parse(idToken);
            String kid = jwt.getHeader().getKeyID();
            // 拉取 Apple 公钥（JWKS）并按 kid 匹配
            JWKSet jwkSet = JWKSet.load(new URL(appleJwksUrl));
            JWK jwk = jwkSet.getKeyByKeyId(kid);
            if (jwk == null) {
                log.warn("[OAuth][Apple] 公钥未匹配 kid={}", kid);
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
            }
            // Apple identityToken 实测 alg=RS256(RSA)；按 JWK 类型选验签器，兼容 EC
            JWSVerifier verifier;
            if (jwk instanceof RSAKey) {
                verifier = new RSASSAVerifier(((RSAKey) jwk).toRSAPublicKey());
            } else if (jwk instanceof ECKey) {
                verifier = new ECDSAVerifier(((ECKey) jwk).toECPublicKey());
            } else {
                log.warn("[OAuth][Apple] 不支持的公钥类型 kid={}", kid);
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
            }
            if (!jwt.verify(verifier)) {
                log.warn("[OAuth][Apple] 验签失败 kid={}", kid);
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
            }
            JWTClaimsSet claims = jwt.getJWTClaimsSet();
            if (!"https://appleid.apple.com".equals(claims.getIssuer())) {
                log.warn("[OAuth][Apple] iss 非法: {}", claims.getIssuer());
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
            }
            List<String> aud = claims.getAudience();
            if (aud == null || !aud.contains(appleClientId)) {
                log.warn("[OAuth][Apple] aud 不匹配: expected={}, got={}", appleClientId, aud);
                throw new ServiceException(MessageUtils.message("no.oauth.audience.mismatch", "Apple"));
            }
            Date exp = claims.getExpirationTime();
            if (exp == null || exp.before(new Date())) {
                log.warn("[OAuth][Apple] token 已过期: exp={}", exp);
                throw new ServiceException(MessageUtils.message("no.oauth.token.expired", "Apple"));
            }
            String sub = claims.getSubject();
            if (sub == null || sub.isEmpty()) {
                log.warn("[OAuth][Apple] sub 为空");
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Apple"));
            }
            log.info("[OAuth][Apple] 校验通过 sub={}", sub);
            return sub;
        } catch (ServiceException se) {
            throw se;
        } catch (Exception e) {
            log.error("[OAuth][Apple] 校验异常", e);
            throw new ServiceException(MessageUtils.message("no.oauth.verify.fail", "Apple", e.getMessage()));
        }
    }

    /** Google：tokeninfo HTTP 验真 + 校 audience，返回 sub。 */
    private String verifyGoogle(String idToken) {
        try {
            log.debug("[OAuth][Google] idToken={}", idToken);
            String url = googleTokeninfoUrl + "?id_token=" + URLEncoder.encode(idToken, "UTF-8");
            JSONObject json = JSONObject.parseObject(httpGet(url, null));
            log.debug("[OAuth][Google] tokeninfo 返回: {}", json);
            if (json == null || json.containsKey("error") || json.containsKey("error_description")) {
                log.warn("[OAuth][Google] tokeninfo 返回错误: {}", json);
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Google"));
            }
            String aud = json.getString("aud");
            if (!googleClientId.equals(aud)) {
                log.warn("[OAuth][Google] aud 不匹配: expected={}, got={}", googleClientId, aud);
                throw new ServiceException(MessageUtils.message("no.oauth.audience.mismatch", "Google"));
            }
            String sub = json.getString("sub");
            if (sub == null || sub.isEmpty()) {
                log.warn("[OAuth][Google] sub 为空");
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "Google"));
            }
            log.info("[OAuth][Google] 校验通过 sub={}", sub);
            return sub;
        } catch (ServiceException se) {
            throw se;
        } catch (Exception e) {
            log.error("[OAuth][Google] 校验异常", e);
            throw new ServiceException(MessageUtils.message("no.oauth.verify.fail", "Google", e.getMessage()));
        }
    }

    /**
     * LINE：authorization code → 换 access_token → v2/profile 取 userId。
     *
     * <p>标准 LINE Login 流程：前端拿到授权 code 后传给后端，后端用 code + clientSecret + clientId
     * 向 {@code https://api.line.me/oauth2/v2.1/token} 换 access_token（form-urlencoded），
     * 再用 access_token 调 v2/profile 取稳定的 userId 作为 providerUid。
     *
     * @param code LINE 授权码（前端授权后获得，一次性）
     * @return LINE userId
     */
    private String verifyLine(String code) {
        try {
            // code 一次性、短期有效；client_secret 永不记录日志
            log.info("[OAuth][LINE] 换 token 请求: grant_type=authorization_code, client_id={}, redirect_uri={}, code={}...(len={})",
                    lineClientId, lineRedirectUri, safePrefix(code), code == null ? 0 : code.length());
            // 1. code 换 access_token（httpPostForm 内会打印 HTTP 状态码 + LINE 原始响应体，含 error/error_description）
            JSONObject token = JSONObject.parseObject(httpPostForm(lineTokenUrl, new String[][]{
                    {"grant_type", "authorization_code"},
                    {"code", code},
                    {"redirect_uri", lineRedirectUri},
                    {"client_id", lineClientId},
                    {"client_secret", lineClientSecret}
            }));
            if (token == null || token.getString("access_token") == null) {
                log.warn("[OAuth][LINE] 换 token 失败（响应无 access_token）: {}", token);
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
            }
            String accessToken = token.getString("access_token");
            log.info("[OAuth][LINE] 换 token 成功: access_token={}...(len={}), scope={}",
                    safePrefix(accessToken), accessToken.length(), token.getString("scope"));
            // 2. access_token 换用户信息（httpGet 内会打印 HTTP 状态码 + profile 原始响应体）
            JSONObject profile = JSONObject.parseObject(httpGet(lineProfileUrl, accessToken));
            if (profile == null) {
                log.warn("[OAuth][LINE] profile 返回空");
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
            }
            String userId = profile.getString("userId");
            if (userId == null || userId.isEmpty()) {
                log.warn("[OAuth][LINE] 无 userId: {}", profile);
                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
            }
            log.info("[OAuth][LINE] 校验通过 userId={}", userId);
            return userId;
        } catch (ServiceException se) {
            throw se;
        } catch (Exception e) {
            log.error("[OAuth][LINE] 校验异常", e);
            throw new ServiceException(MessageUtils.message("no.oauth.verify.fail", "LINE", e.getMessage()));
        }
    }

    /** 简单 GET；bearer 非空时带 Authorization 头（LINE 用）。带超时，防止 provider 慢/不可达拖垮线程。 */
    private String httpGet(String url, String bearer) throws Exception {
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet get = new HttpGet(url);
            get.setConfig(RequestConfig.custom()
                    .setConnectTimeout(5000)
                    .setSocketTimeout(10000)
                    .setConnectionRequestTimeout(5000)
                    .build());
            if (bearer != null && !bearer.isEmpty()) {
                get.setHeader("Authorization", "Bearer " + bearer);
            }
            try (CloseableHttpResponse resp = client.execute(get)) {
                int status = resp.getStatusLine().getStatusCode();
                String body = EntityUtils.toString(resp.getEntity(), "UTF-8");
                // google tokeninfo 的 url 含 id_token，只记 host 避免 JWT 刷屏
                String host;
                try {
                    host = new URL(url).getHost();
                } catch (Exception e) {
                    host = url;
                }
                log.info("[OAuth][HTTP] GET host={} status={} body={}", host, status, body);
                return body;
            }
        }
    }

    /** 简单 POST application/x-www-form-urlencoded（LINE 换 token 用）。带超时，防止 LINE 慢/不可达拖垮线程。 */
    private String httpPostForm(String url, String[][] form) throws Exception {
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpPost post = new HttpPost(url);
            post.setConfig(RequestConfig.custom()
                    .setConnectTimeout(5000)
                    .setSocketTimeout(10000)
                    .setConnectionRequestTimeout(5000)
                    .build());
            List<NameValuePair> pairs = new ArrayList<>();
            for (String[] kv : form) {
                pairs.add(new BasicNameValuePair(kv[0], kv[1]));
            }
            post.setEntity(new UrlEncodedFormEntity(pairs, "UTF-8"));
            try (CloseableHttpResponse resp = client.execute(post)) {
                int status = resp.getStatusLine().getStatusCode();
                String body = EntityUtils.toString(resp.getEntity(), "UTF-8");
                // LINE 换 token 失败时 status=400，body 含 error/error_description，是定位根因的关键
                log.info("[OAuth][HTTP] POST {} status={} body={}", url, status, body);
                return body;
            }
        }
    }

    /** 取前 8 字符做日志脱敏（code/access_token 不全文打印）。 */
    private static String safePrefix(String s) {
        return s == null ? "" : s.substring(0, Math.min(8, s.length()));
    }
}
