/**
 * 支付工具（审查优化版）
 * 修复项：
 *  - randomStr 消除取模偏差（crypto.randomInt）
 *  - amountToFen 严格金额校验，拒绝浮点/科学计数法/溢出
 *  - parseXml 支持 CDATA 边界与实体，防畸形 XML 漏解析
 *  - 签名比较使用常量时间比较（防侧信道）
 *  - httpsPost 增加最大响应体限制（防 OOM）与超时
 */
const crypto = require('crypto');
const https = require('https');
const { URL } = require('url');

const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const CHARS_LEN = CHARS.length;

/** 生成随机字符串（无取模偏差） */
function randomStr(len = 32) {
  let out = '';
  for (let i = 0; i < len; i++) {
    out += CHARS[crypto.randomInt(CHARS_LEN)];
  }
  return out;
}

/**
 * 金额(元)转分（整数）。
 * 安全：只接受有限小数点字符串/数字，拒绝科学计数法与溢出。
 * @returns {number} 分（整数），非法输入返回 NaN 需调用方校验
 */
function amountToFen(amount) {
  if (typeof amount === 'number') {
    if (!Number.isFinite(amount)) return NaN;
    amount = String(amount);
  }
  if (typeof amount !== 'string') return NaN;
  // 仅允许 数字+最多2位小数
  if (!/^\d+(\.\d{1,2})?$/.test(amount)) return NaN;
  const [int, dec = ''] = amount.split('.');
  const intPart = BigInt(int || '0');
  const fraction = Number((dec + '00').slice(0, 2)); // 不足两位补0
  const fen = intPart * 100n + BigInt(fraction);
  if (fen > BigInt(Number.MAX_SAFE_INTEGER)) return NaN; // 防溢出
  return Number(fen);
}

/** 常量时间字符串比较（防时序侧信道） */
function safeEqual(a, b) {
  const ba = Buffer.from(String(a), 'utf8');
  const bb = Buffer.from(String(b), 'utf8');
  if (ba.length !== bb.length) return false;
  return crypto.timingSafeEqual(ba, bb);
}

/** 微信签名（MD5 v2） */
function wechatSign(params, key) {
  return crypto.createHash('md5').update(buildSignString(params) + '&key=' + key, 'utf8').digest('hex').toUpperCase();
}

/** 微信签名（HMAC-SHA256） */
function wechatSignHmac(params, key) {
  return crypto.createHmac('sha256', key).update(buildSignString(params) + '&key=' + key, 'utf8').digest('hex').toUpperCase();
}

/** 微信回调验签（常量时间比较） */
function verifyWechatNotify(data, key) {
  const signType = (data.sign_type || 'MD5').toUpperCase();
  const calc = signType === 'HMAC-SHA256' ? wechatSignHmac(data, key) : wechatSign(data, key);
  return safeEqual(calc, data.sign || '');
}

/**
 * 构造签名串。
 * @param {object} params
 * @param {object} opts {excludeSignType:boolean} 支付宝需包含 sign_type，微信排除
 */
function buildSignString(params, opts = {}) {
  return Object.keys(params)
    .filter(k => {
      const v = params[k];
      if (v === '' || v === null || v === undefined || k === 'sign' || k === 'key') return false;
      if (opts.excludeSignType !== false && k === 'sign_type') return false;
      return true;
    })
    .sort()
    .map(k => `${k}=${params[k]}`)
    .join('&');
}

/** 构建微信请求 XML（CDATA 包裹，转义边界） */
function buildXml(root, obj) {
  let xml = `<${root}>`;
  for (const [k, v] of Object.entries(obj)) {
    if (v === null || v === undefined) continue;
    // 转义 CDATA 结束标记，防注入
    const val = String(v).split(']]>').join(']]]]><![CDATA[>');
    xml += `<${k}><![CDATA[${val}]]></${k}>`;
  }
  return xml + `</${root}>`;
}

/**
 * 解析微信 XML（扁平结构）。健壮版：
 *  - 支持 CDATA 与普通文本，正确处理 ]]> 边界
 *  - 返回 k/v 对象
 */
function parseXml(xml) {
  const res = {};
  const re = /<(\w+)>(?:<!\[CDATA\[([\s\S]*?)\]\]>|([^<]*))?<\/\1>/g;
  let m;
  while ((m = re.exec(xml)) !== null) {
    res[m[1]] = m[2] !== undefined ? m[2] : m[3];
  }
  return res;
}

/** 支付宝签名（RSA-SHA256，私钥） */
function alipaySign(params, privateKey) {
  // 支付宝规范：包含 sign_type 参与排序（sign_type=RSA2）
  const str = buildSignString(params, { excludeSignType: false });
  return crypto.createSign('RSA-SHA256').update(str, 'utf8').sign(privateKey, 'base64');
}

/** 支付宝验签（RSA-SHA256，公钥） */
function verifyAlipaySign(data, publicKey) {
  const sign = data.sign || '';
  // 支付宝验签同样包含 sign_type
  const str = buildSignString(data, { excludeSignType: false });
  return crypto.createVerify('RSA-SHA256').update(str, 'utf8').verify(publicKey, sign, 'base64');
}

/** HTTPS POST（无证书），带最大响应体限制 */
function httpsPost(url, body, contentType = 'application/json', { timeout = 15000, maxBody = 5 * 1024 * 1024 } = {}) {
  return new Promise((resolve, reject) => {
    try {
      const u = new URL(url);
      const data = typeof body === 'string' ? body : JSON.stringify(body);
      const req = https.request({
        hostname: u.hostname,
        port: u.port || 443,
        path: u.pathname + u.search,
        method: 'POST',
        headers: {
          'Content-Type': contentType,
          'Content-Length': Buffer.byteLength(data),
        },
        rejectUnauthorized: true,
        timeout,
      }, (res) => {
        res.setEncoding('utf8');
        let chunks = [];
        let size = 0;
        res.on('data', (c) => {
          size += Buffer.byteLength(c);
          if (size > maxBody) {
            req.destroy();
            return reject(new Error('响应体超过限制'));
          }
          chunks.push(c);
        });
        res.on('end', () => resolve({ status: res.statusCode, data: chunks.join('') }));
      });
      req.on('error', reject);
      req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
      req.write(data);
      req.end();
    } catch (e) {
      reject(e);
    }
  });
}

/** HTTPS POST（带客户端证书，微信退款用） */
function httpsPostWithCert(url, body, certPath = '', certKeyPath = '', { timeout = 15000, maxBody = 5 * 1024 * 1024 } = {}) {
  return new Promise((resolve, reject) => {
    try {
      const u = new URL(url);
      const fs = require('fs');
      const data = typeof body === 'string' ? body : JSON.stringify(body);
      const opts = {
        hostname: u.hostname,
        port: u.port || 443,
        path: u.pathname + u.search,
        method: 'POST',
        headers: {
          'Content-Type': 'text/xml',
          'Content-Length': Buffer.byteLength(data),
        },
        rejectUnauthorized: true,
        timeout,
      };
      if (certPath && certKeyPath) {
        opts.cert = fs.readFileSync(certPath);
        opts.key = fs.readFileSync(certKeyPath);
      }
      const req = https.request(opts, (res) => {
        res.setEncoding('utf8');
        let chunks = [];
        let size = 0;
        res.on('data', (c) => {
          size += Buffer.byteLength(c);
          if (size > maxBody) {
            req.destroy();
            return reject(new Error('响应体超过限制'));
          }
          chunks.push(c);
        });
        res.on('end', () => resolve({ status: res.statusCode, data: chunks.join('') }));
      });
      req.on('error', reject);
      req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
      req.write(data);
      req.end();
    } catch (e) {
      reject(e);
    }
  });
}

module.exports = {
  randomStr,
  amountToFen,
  safeEqual,
  wechatSign,
  wechatSignHmac,
  verifyWechatNotify,
  buildSignString,
  buildXml,
  parseXml,
  alipaySign,
  verifyAlipaySign,
  httpsPost,
  httpsPostWithCert,
};
