import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * NetcCheck 官方签名模块 · Java（JDK 8+，零依赖）
 *
 * 用法：
 *   TreeMap<String, String> body = new TreeMap<>();
 *   body.put("appkey", APPKEY);
 *   body.put("time", String.valueOf(System.currentTimeMillis() / 1000));
 *   body.put("nonce", UUID.randomUUID().toString().replace("-", ""));
 *   body.put("username", "tom");            // ...其余业务参数
 *   body.put("sign", SignModule.sign(body, SECRET));
 *   String resp = httpPostJson(API + "/user/login", body);   // 发请求见接入文档示例
 *   if (!SignModule.verifyRsign(resp, body.get("nonce"), SECRET))
 *       throw new IllegalStateException("响应来源不可信（疑似假服务器），已中止");
 *
 * 设备会话模式（推荐，见接入文档「设备会话」章节）：
 *   本模块对会话签名完全适用——签名流程不变，只换密钥：
 *     ① 设备注册   body.put("sign", SignModule.sign(body, APPSECRET));   // /device/enroll，AppSecret 仅此一步
 *     ② 换取会话   body.put("sign", SignModule.sign(body, deviceSecret)); // /device/session，含 deviceId 参数
 *     ③ 会话内业务 body.put("sid", sessionId); body.put("sign", SignModule.sign(body, sessionKey));
 *   响应验签密钥跟随当前签名密钥：会话请求传 sessionKey，未带 sid 的响应用 AppSecret。
 *   strict_mode 软件的 AppSecret 仅用于设备注册，泄露也无法伪造业务请求。
 *
 * 约定：
 *   - sign() 对除 "sign" 本身外的全部参数计算签名；开启传输加密后 enc/iv/data 同样参与；会话请求的 sid 也参与。
 *   - 参数值一律以字符串传入（时间戳、enc 等与接入文档示例保持一致）。
 *   - 算法细节随平台升级可能调整，请勿依据旧版资料自行实现——以本模块为准。
 *   - 免费提供，商用授权随套餐附送。获取/更新：控制台「总览 → 接入信息」或联系客服。
 */
public final class SignModule {

    private SignModule() {
    }

    /** 签名基准串：除 sign 外的全部参数按参数名升序 k=v 用 & 连接，末尾拼 "&key=secret" */
    private static String canonical(Map<String, String> params, String secret) {
        List<String> keys = new ArrayList<String>();
        for (String k : params.keySet()) if (!"sign".equals(k)) keys.add(k);
        Collections.sort(keys);
        StringBuilder sb = new StringBuilder();
        for (String k : keys) {
            if (sb.length() > 0) sb.append('&');
            sb.append(k).append('=').append(params.get(k));
        }
        return sb.append("&key=").append(secret).toString();
    }

    /** 计算请求签名（HMAC-SHA256，返回 hex 小写）。params 中 sign 键会被忽略。 */
    public static String sign(Map<String, String> params, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            return hex(mac.doFinal(canonical(params, secret).getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new IllegalStateException("签名计算失败", e);
        }
    }

    /**
     * 验证响应签名（防「改 hosts 假服务器恒返 200」攻击）。
     * respJson 为原始响应文本；nonce 为本次请求使用的 nonce。
     * key = 本次请求使用的签名密钥：常规请求传 AppSecret；设备会话请求（带 sid）传 sessionKey。
     * 返回 false = 响应不可信，立即中止，不要使用其中的任何数据。
     * 仅 200 响应需要且必须验签（鉴权未通过的失败响应不带 rsign）。
     */
    public static boolean verifyRsign(String respJson, String nonce, String key) {
        String rsign = extractString(respJson, "rsign");
        if (rsign == null || rsign.isEmpty()) return false;
        String code = extractNumber(respJson, "code");
        String msg = unescape(extractString(respJson, "msg"));
        String message = "ncrs|" + nonce + "|" + (code == null ? "" : code) + "|" + (msg == null ? "" : msg);
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            String expect = hex(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
            return MessageDigest.isEqual(expect.getBytes(StandardCharsets.UTF_8), rsign.toLowerCase().getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            return false;
        }
    }

    /** 极简 JSON 字符串字段提取（返回不含引号的内容；生产环境建议改用 JSON 库） */
    private static String extractString(String json, String field) {
        Matcher m = Pattern.compile("\"" + field + "\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"").matcher(json);
        return m.find() ? m.group(1) : null;
    }

    /** 极简 JSON 数字字段提取 */
    private static String extractNumber(String json, String field) {
        Matcher m = Pattern.compile("\"" + field + "\"\\s*:\\s*(-?\\d+)").matcher(json);
        return m.find() ? m.group(1) : null;
    }

    private static String unescape(String s) {
        if (s == null) return null;
        return s.replace("\\\"", "\"").replace("\\\\", "\\");
    }

    private static String hex(byte[] data) {
        StringBuilder sb = new StringBuilder(data.length * 2);
        for (byte b : data) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    /**
     * 签名 v2（对接简化版，推荐新接入使用）：sign = HMAC-SHA256(secret, 原始请求体字符串)。
     * 先把含 sver=2 与业务参数的最终 JSON 序列化好，对原文签名后再拼入 sign 字段一并发送；
     * 请求体必须与签名时逐字节一致。仅 HMAC-SHA256，无 MD5 路径。密钥选择与 sign() 相同。
     */
    public static String signV2(String rawBody, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
            return hex(mac.doFinal(rawBody.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
    /** 由 AppSecret 派生传输加密密钥（enc_key 留空的软件；HKDF-SHA256，与服务器一致），返回 64 位 hex。 */
    public static String deriveEncKey(String appsecretHex, String appkey) {
        try {
            Mac prk = Mac.getInstance("HmacSHA256");
            prk.init(new SecretKeySpec("netccheck-enc-v1".getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] p = prk.doFinal(hexToBytes(appsecretHex));
            Mac okm = Mac.getInstance("HmacSHA256");
            okm.init(new SecretKeySpec(p, "HmacSHA256"));
            okm.update(("nk:" + appkey).getBytes(java.nio.charset.StandardCharsets.UTF_8));
            okm.update(new byte[]{1});
            return hex(okm.doFinal());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static byte[] hexToBytes(String h) {
        byte[] out = new byte[h.length() / 2];
        for (int i = 0; i < out.length; i++) out[i] = (byte) Integer.parseInt(h.substring(i * 2, i * 2 + 2), 16);
        return out;
    }
}}
