// ============================================================
// AlipayProvider — 支付宝开放平台 HTTP API 封装
//
// ── 支付宝 OAuth 移动应用授权流程 ───────────────────────────
//   Step 1 (客户端 tobias 包):
//     调用 Alipay.authCode() 拉起支付宝 App
//     用户点「确认授权」→ 支付宝 App 回传 auth_code
//     auth_code 一次性，有效期 3 分钟
//
//   Step 2 (本文件 exchangeAuthCode):
//     POST https://openapi.alipay.com/gateway.do
//     method=alipay.system.oauth.token
//     grant_type=authorization_code&code=AUTH_CODE
//     返回: access_token, user_id, expires_in
//
//   Step 3 (本文件 getUserInfo):
//     POST https://openapi.alipay.com/gateway.do
//     method=alipay.user.info.share
//     auth_token=ACCESS_TOKEN
//     返回: user_id, nick_name, avatar
//
// ── 申请支付宝 AppID 步骤 ───────────────────────────────────
// 1. 登录支付宝开放平台
//    https://open.alipay.com
//    → 控制台 → 创建应用 → 移动应用 → 填写 App 名称 / 图标 / Bundle ID
//
// 2. 配置密钥
//    应用详情 → 开发设置 → 接口加签方式 → RSA2（推荐）
//    用「支付宝开放平台密钥工具」生成 RSA2 密钥对（2048位）：
//    - 将「应用公钥」上传到开放平台
//    - 将「应用私钥」配置到服务器 .env（ALIPAY_PRIVATE_KEY）
//    - 将「支付宝公钥」（平台提供的，用于验证响应签名）配置到 .env（ALIPAY_PUBLIC_KEY）
//
// 3. 开通「获取会员信息」接口
//    应用详情 → 添加功能 → 获取会员信息（alipay.user.info.share）
//    → 签约（需审核，通常1个工作日内）
//
// 4. 获取 AppID（16位数字，如 2021003189xxxxxx）
//
// ── 环境变量配置 ───────────────────────────────────────────
//   ALIPAY_APP_ID          — 支付宝应用 AppID（16位数字）
//   ALIPAY_PRIVATE_KEY     — 应用 RSA2 私钥（PKCS8 格式，无换行符，Base64）
//   ALIPAY_PUBLIC_KEY      — 支付宝平台公钥（用于验证响应签名）
//   ALIPAY_GATEWAY         — 网关 URL（正式: openapi.alipay.com，沙箱: openapi-sandbox.dl.alipaydev.com）
//
// ── RSA2 签名算法（SHA256WithRSA）─────────────────────────
//   1. 将所有请求参数（除 sign）按 key 字典序升序排列
//   2. 拼接为 key=value&key2=value2（不 URL 编码）
//   3. 用 RSA2 私钥（SHA256WithRSA）对拼接字符串签名
//   4. Base64 编码签名结果后 URL 编码，加入 sign 参数
//
// ── 常见错误码 ─────────────────────────────────────────────
//   40001: auth_code 无效或已使用
//   40006: auth_code 已过期
//   20001: alipay.user.info.share 接口未开通
// ============================================================

import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import * as https from 'https';
import * as crypto from 'crypto';
import * as querystring from 'querystring';

/** 支付宝 token 接口业务响应 */
export interface AlipayTokenResponse {
  user_id: string;
  access_token: string;
  expires_in: number;
  refresh_token: string;
  re_expires_in: number;
}

/** 支付宝用户信息业务响应 */
export interface AlipayUserInfo {
  user_id: string;
  nick_name?: string;
  avatar?: string;
  gender?: string;   // m=男 f=女
  province?: string;
  city?: string;
  is_student_certified?: string;
  user_type?: string;   // 1=个人用户 2=企业用户
}

@Injectable()
export class AlipayProvider {
  private readonly logger = new Logger('AlipayProvider');

  private readonly appId = process.env.ALIPAY_APP_ID;
  // PID（Partner ID）：支付宝商户号，16位数字（2088xxxxxxxxxx），
  // 在支付宝开放平台「账号信息」页面查看。
  // 若未配置则降级使用 appId（部分沙箱环境可行，正式环境必须配置真实 PID）
  private readonly pid = process.env.ALIPAY_PID || process.env.ALIPAY_APP_ID;
  private readonly privateKey = process.env.ALIPAY_PRIVATE_KEY;
  private readonly gateway =
    process.env.ALIPAY_GATEWAY || 'openapi.alipay.com';

  /**
   * 生成移动端 App 授权字符串（供 tobias Flutter 包使用）
   *
   * tobias 3.x+ 的 `Tobias().auth(authString)` 需要传入一个由服务端签名的
   * 授权请求字符串，而非直接传 appId+scope。这是 Alipay SDK 的安全升级要求：
   * 将签名计算移到服务端，防止客户端泄露私钥。
   *
   * 生成的 authString 传给移动端后，由 Alipay SDK 解析并拉起支付宝 App，
   * 用户授权后支付宝 App 回传 auth_code，再走后端 POST /auth/alipay 流程。
   *
   * 参数格式（字典序排列，URL 编码，最后附 sign）：
   *   apiname=com.alipay.account.auth
   *   app_id=2021xxxxxxxx
   *   biz_type=openservice
   *   charset=UTF-8
   *   pid=2088xxxxxxxx  ← 商户 PID（ALIPAY_PID 环境变量，非 app_id）
   *   product_id=APP_FAST_LOGIN
   *   scope=auth_user   ← 或 kuaijie（静默授权）
   *   sign_type=RSA2
   *   timestamp=2024-01-15 10:30:00
   *   sign=BASE64SIGNATURE
   *
   * @param scope 'auth_user'（获取用户信息，需授权页）| 'kuaijie'（静默，仅 user_id）
   */
  generateMobileAuthString(scope: string = 'auth_user'): string {
    const timestamp = new Date()
      .toISOString()
      .replace('T', ' ')
      .substring(0, 19);

    const params: Record<string, string> = {
      apiname: 'com.alipay.account.auth',
      app_id: this.appId!,
      biz_type: 'openservice',
      charset: 'UTF-8',
      pid: this.pid!,
      product_id: 'APP_FAST_LOGIN',
      scope,
      sign_type: 'RSA2',
      timestamp,
    };

    // 签名：与网关请求签名算法一致（字典序拼接 → RSA-SHA256 → Base64）
    const sign = this.sign(params);

    // 拼接最终 authString（字典序排列，值 URL 编码，末尾附 sign）
    const sortedKeys = Object.keys(params).sort();
    const encoded = sortedKeys
      .map((k) => `${k}=${encodeURIComponent(params[k])}`)
      .join('&');
    return `${encoded}&sign=${encodeURIComponent(sign)}`;
  }

  /**
   * Step 2: 用 auth_code 换取 access_token
   *
   * API: alipay.system.oauth.token (grant_type=authorization_code)
   */
  async exchangeAuthCode(authCode: string): Promise<AlipayTokenResponse> {
    const result = await this.callGateway('alipay.system.oauth.token', {
      grant_type: 'authorization_code',
      code: authCode,
    });

    const resp = result['alipay_system_oauth_token_response'] as Record<string, unknown> | undefined;
    if (!resp || result['error_response']) {
      const err = (result['error_response'] || {}) as Record<string, unknown>;
      this.logger.error(`Alipay token exchange failed: ${JSON.stringify(err)}`);
      throw new BadRequestException(
        `支付宝授权失败: ${err['sub_msg'] || err['msg'] || '未知错误'} (code: ${err['code'] || '?'})`,
      );
    }

    this.logger.log(`Alipay token exchanged: userId=${resp['user_id']}`);
    return resp as unknown as AlipayTokenResponse;
  }

  /**
   * Step 3: 用 access_token 获取用户信息
   *
   * API: alipay.user.info.share (需开通「获取会员信息」接口权限)
   */
  async getUserInfo(accessToken: string): Promise<AlipayUserInfo> {
    const result = await this.callGateway(
      'alipay.user.info.share',
      {},
      accessToken,
    );

    const resp = result['alipay_user_info_share_response'] as Record<string, unknown> | undefined;
    if (!resp || result['error_response'] || resp['code'] !== '10000') {
      const err = (result['error_response'] || resp || {}) as Record<string, unknown>;
      this.logger.error(`Alipay userinfo failed: ${JSON.stringify(err)}`);
      throw new BadRequestException(
        `获取支付宝用户信息失败: ${err['sub_msg'] || err['msg'] || '请确认已开通「获取会员信息」接口权限'}`,
      );
    }

    return resp as unknown as AlipayUserInfo;
  }

  /**
   * 调用支付宝开放平台网关
   *
   * 支付宝所有 API 共用同一个网关 URL（openapi.alipay.com/gateway.do），
   * 通过 method 参数区分具体接口，所有请求必须携带 RSA2 签名。
   *
   * 请求参数结构（POST application/x-www-form-urlencoded）：
   *   app_id      — 应用 AppID
   *   method      — 接口名称，如 alipay.system.oauth.token
   *   charset     — 编码，固定 utf-8
   *   sign_type   — 签名算法，固定 RSA2（推荐，比 RSA 更安全）
   *   timestamp   — 发送请求的时间（yyyy-MM-dd HH:mm:ss，北京时间）
   *   version     — API 版本，固定 1.0
   *   biz_content — JSON 格式业务参数（仅有业务参数时才传）
   *   auth_token  — 用户授权 Token（调用需要授权的接口时传）
   *   sign        — 对以上所有参数的 RSA2 签名（最后计算，不参与签名本身）
   *
   * @param method      支付宝开放平台接口名称
   * @param bizContent  业务参数（将被 JSON.stringify 后放入 biz_content）
   * @param authToken   用户 access_token（获取用户信息时传入）
   */
  private async callGateway(
    method: string,
    bizContent: Record<string, string>,
    authToken?: string,
  ): Promise<Record<string, unknown>> {
    // 时间格式: "2024-01-15 10:30:00"（北京时间，去掉 T 和毫秒）
    const timestamp = new Date()
      .toISOString()
      .replace('T', ' ')
      .substring(0, 19);

    // 公共参数（每次请求都必须携带）
    const params: Record<string, string> = {
      app_id: this.appId!,
      method,
      charset: 'utf-8',
      sign_type: 'RSA2',
      timestamp,
      version: '1.0',
    };

    // auth_token: 调用 alipay.user.info.share 时必须携带，代表用户授权
    if (authToken) {
      params['auth_token'] = authToken;
    }

    // biz_content: 业务参数（JSON 格式），部分接口（如 token 换取）需要
    if (Object.keys(bizContent).length > 0) {
      params['biz_content'] = JSON.stringify(bizContent);
    }

    // 最后一步：对所有参数（除 sign 本身）计算 RSA2 签名
    params['sign'] = this.sign(params);

    // POST body: application/x-www-form-urlencoded 格式
    // querystring.stringify 会自动对值进行 URL 编码
    const postBody = querystring.stringify(params);

    const data = await this.httpsPost(this.gateway, postBody);
    return JSON.parse(data) as Record<string, unknown>;
  }

  /**
   * RSA2 签名算法（SHA256WithRSA）
   *
   * 支付宝官方规范：
   *   1. 收集所有请求参数（去掉 sign、值为空的参数）
   *   2. 按参数名的字典序（ASCII 升序）排列
   *   3. 拼接为 "key=value&key2=value2"（值不做 URL 编码）
   *   4. 用 RSA2 私钥（PKCS8 格式）对拼接后的字符串做 SHA256WithRSA 签名
   *   5. 将签名结果 Base64 编码，URL 编码后放入 sign 参数
   *
   * 示例签名字符串（字典序排列）：
   *   app_id=2021003189&biz_content={"code":"AP_xxx"}&charset=utf-8&
   *   method=alipay.system.oauth.token&sign_type=RSA2&timestamp=2024-01-15 10:30:00&version=1.0
   *
   * 私钥格式注意：
   *   ALIPAY_PRIVATE_KEY 存储的是纯 Base64 字符串（无 -----BEGIN PRIVATE KEY----- 头尾）
   *   Node.js crypto 需要完整 PEM 格式，因此需要加头尾并每64字符换行
   */
  private sign(params: Record<string, string>): string {
    // Step 1+2: 过滤空值和 sign 参数，按字典序排列
    const sortedKeys = Object.keys(params)
      .filter((k) => k !== 'sign' && params[k] !== '' && params[k] != null)
      .sort();

    // Step 3: 拼接 key=value（注意：值保持原始字符串，不 URL 编码）
    const signString = sortedKeys.map((k) => `${k}=${params[k]}`).join('&');

    // Step 4: 构建 PEM 格式私钥（Node.js crypto 要求标准 PEM 格式）
    // ALIPAY_PRIVATE_KEY 是 PKCS8 格式，使用 -----BEGIN PRIVATE KEY-----
    // 注意: 若是 PKCS1 格式则用 -----BEGIN RSA PRIVATE KEY-----
    const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${this.chunkBase64(this.privateKey!)}\n-----END PRIVATE KEY-----`;

    // Step 4: RSA-SHA256 = SHA256WithRSA = RSA2（支付宝的叫法）
    const signer = crypto.createSign('RSA-SHA256');
    signer.update(signString, 'utf8');

    // Step 5: Base64 编码签名结果
    return signer.sign(privateKeyPem, 'base64');
  }

  /**
   * 将纯 Base64 字符串按每64字符换行，生成 PEM 格式所需的多行格式
   *
   * PEM 标准（RFC 7468）要求 Base64 内容每行不超过64字符，
   * 支付宝密钥工具导出的私钥可能是单行，必须转换后才能被 Node.js crypto 识别。
   */
  private chunkBase64(base64: string): string {
    return base64.match(/.{1,64}/g)?.join('\n') || base64;
  }

  /** HTTPS POST */
  private httpsPost(host: string, body: string): Promise<string> {
    return new Promise((resolve, reject) => {
      const options = {
        hostname: host,
        path: '/gateway.do',
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
          'Content-Length': Buffer.byteLength(body),
        },
      };
      const req = https.request(options, (res) => {
        let data = '';
        res.on('data', (chunk) => (data += chunk));
        res.on('end', () => resolve(data));
      });
      req.on('error', reject);
      req.setTimeout(10000, () => {
        req.destroy();
        reject(new Error('Alipay API timeout'));
      });
      req.write(body);
      req.end();
    });
  }
}
