import crypto from 'node:crypto';
import { HttpError } from '../../core/http.js';
import { config as appConfig } from '../../core/config.js';
import {
  optionalPublicBoolean,
  optionalPublicString,
  providerEndpoint,
  providerEndpointForKeys,
  requirePublicString,
  requireSecretString,
  type TenantPaymentProviderConfig,
} from '../../core/tenant-provider-config.js';

export type PaymentProviderName = 'wechat_pay' | 'alipay' | 'manual';

export interface PaymentOrderInput {
  tenantId: string;
  orderId: string;
  orderNo: string;
  description: string;
  amountCents: number;
  userId: string;
  openId?: string;
  clientIp?: string;
  returnUrl?: string;
  quitUrl?: string;
}

export interface PaymentRefundInput {
  tenantId: string;
  orderId: string;
  orderNo: string;
  refundNo: string;
  amountCents: number;
  totalAmountCents: number;
  providerTradeNo?: string | null;
  reason?: string | null;
  notifyUrl?: string | null;
}

export interface PaymentRefundQueryInput {
  tenantId: string;
  orderId: string;
  orderNo: string;
  refundNo: string;
  amountCents: number;
  totalAmountCents: number;
  providerTradeNo?: string | null;
  providerRefundNo?: string | null;
}

export interface PaymentCreateResult {
  provider: PaymentProviderName;
  method: string;
  providerTradeNo?: string;
  paymentParams: Record<string, unknown>;
  raw: Record<string, unknown>;
}

export interface PaymentRefundResult {
  provider: PaymentProviderName;
  status: 'processing' | 'succeeded' | 'failed';
  providerRefundNo?: string;
  failureReason?: string;
  raw: Record<string, unknown>;
}

export interface PaymentRefundNotificationResult extends PaymentRefundResult {
  eventId: string;
  eventType: string;
  orderNo: string;
  refundNo: string;
  amountCents: number;
  notifiedAt?: string;
}

export interface PaymentNotificationResult {
  provider: PaymentProviderName;
  eventId: string;
  eventType: string;
  orderNo: string;
  providerTradeNo: string;
  amountCents: number;
  paidAt?: string;
  raw: Record<string, unknown>;
}

export interface PaymentProvider {
  name: PaymentProviderName;
  createPayment(input: PaymentOrderInput): Promise<PaymentCreateResult>;
  createRefund(input: PaymentRefundInput): Promise<PaymentRefundResult>;
  queryRefund(input: PaymentRefundQueryInput): Promise<PaymentRefundResult>;
  parseNotification(input: {
    headers: Record<string, string>;
    body: Record<string, unknown>;
    rawBody: string;
  }): Promise<PaymentNotificationResult>;
  parseRefundNotification(input: {
    headers: Record<string, string>;
    body: Record<string, unknown>;
    rawBody: string;
  }): Promise<PaymentRefundNotificationResult>;
}

function base64urlDecode(value: string) {
  return Buffer.from(value.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
}

function objectValue(value: unknown): Record<string, unknown> {
  return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}

function requiredBodyString(body: Record<string, unknown>, key: string, code: string) {
  const value = body[key];
  if (typeof value === 'string' && value.trim()) return value.trim();
  throw new HttpError(400, `${key} is required`, code);
}

function optionalBodyString(body: Record<string, unknown>, key: string) {
  const value = body[key];
  return typeof value === 'string' && value.trim() ? value.trim() : '';
}

function rsaSignSha256(privateKey: string, message: string) {
  return crypto.createSign('RSA-SHA256').update(message, 'utf8').sign(privateKey, 'base64');
}

function rsaVerifySha256(publicKey: string, message: string, signature: string) {
  return crypto.createVerify('RSA-SHA256').update(message, 'utf8').verify(publicKey, signature, 'base64');
}

function normalizePem(value: string, label: 'PRIVATE KEY' | 'PUBLIC KEY') {
  if (value.includes('-----BEGIN')) return value;
  const wrapped = value.match(/.{1,64}/g)?.join('\n') || value;
  return `-----BEGIN ${label}-----\n${wrapped}\n-----END ${label}-----`;
}

function safeJson(value: unknown) {
  return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}

function wechatRefundStatus(value: unknown) {
  const status = typeof value === 'string' ? value.toUpperCase() : '';
  if (status === 'SUCCESS') return 'succeeded';
  if (['CLOSED', 'ABNORMAL'].includes(status)) return 'failed';
  return 'processing';
}

function alipayRefundStatus(value: Record<string, unknown>) {
  const refundStatus = typeof value.refund_status === 'string' ? value.refund_status.toUpperCase() : '';
  const fundChange = typeof value.fund_change === 'string' ? value.fund_change.toUpperCase() : '';
  if (refundStatus === 'REFUND_SUCCESS' || fundChange === 'Y') return 'succeeded';
  if (['REFUND_CLOSED', 'REFUND_FAIL'].includes(refundStatus)) return 'failed';
  return 'processing';
}

function optionalIntegerAmount(value: unknown) {
  const amount = typeof value === 'number' ? value : Number.NaN;
  return Number.isInteger(amount) && amount >= 0 ? amount : null;
}

function optionalYuanToCents(value: unknown) {
  if (typeof value !== 'string' && typeof value !== 'number') return null;
  const amount = Number(value);
  if (!Number.isFinite(amount) || amount < 0) return null;
  return Math.round(amount * 100);
}

function assertProviderRefundAmount(actualCents: number | null, expectedCents: number, code = 'PAYMENT_PROVIDER_REFUND_AMOUNT_MISMATCH') {
  if (actualCents !== null && actualCents !== expectedCents) {
    throw new HttpError(502, 'Payment provider refund amount mismatch', code);
  }
}

function canonicalForm(params: Record<string, string>) {
  return Object.keys(params)
    .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
    .sort()
    .map(key => `${key}=${params[key]}`)
    .join('&');
}

function encodedForm(params: Record<string, string>) {
  return Object.keys(params)
    .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
    .sort()
    .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
    .join('&');
}

function wechatPrivateKey(config: TenantPaymentProviderConfig) {
  return normalizePem(requireSecretString(config, ['privateKey', 'merchantPrivateKey'], 'PAYMENT_SECRET_REQUIRED'), 'PRIVATE KEY');
}

function randomNonce(size = 16) {
  return crypto.randomBytes(size).toString('base64url');
}

function verifyWechatNotification(
  config: TenantPaymentProviderConfig,
  input: {
    headers: Record<string, string>;
    body: Record<string, unknown>;
    rawBody: string;
  },
) {
  const publicKey = optionalPublicString(config, ['wechatpayPublicKey', 'platformCertificatePublicKey'])
    || requireSecretString(config, ['wechatpayPublicKey', 'platformCertificatePublicKey', 'publicKey'], 'PAYMENT_SECRET_REQUIRED');
  const allowMockSignature = !appConfig.isProduction && optionalPublicBoolean(config, 'allowLocalMock', false);
  if (publicKey && input.headers['wechatpay-signature']) {
    const timestamp = input.headers['wechatpay-timestamp'] || '';
    const nonceHeader = input.headers['wechatpay-nonce'] || '';
    const message = `${timestamp}\n${nonceHeader}\n${input.rawBody}\n`;
    if (!rsaVerifySha256(normalizePem(publicKey, 'PUBLIC KEY'), message, input.headers['wechatpay-signature'])) {
      throw new HttpError(400, 'WeChat Pay notification signature invalid', 'PAYMENT_SIGNATURE_INVALID');
    }
  } else if (!allowMockSignature) {
    throw new HttpError(400, 'WeChat Pay notification signature is required', 'PAYMENT_SIGNATURE_REQUIRED');
  }
}

function decryptWechatNotificationResource(config: TenantPaymentProviderConfig, body: Record<string, unknown>) {
  const apiV3Key = requireSecretString(config, ['apiV3Key', 'apiv3Key'], 'PAYMENT_SECRET_REQUIRED');
  const resource = objectValue(body.resource);
  const ciphertext = requiredBodyString(resource, 'ciphertext', 'PAYMENT_NOTIFICATION_INVALID');
  const nonce = requiredBodyString(resource, 'nonce', 'PAYMENT_NOTIFICATION_INVALID');
  const associatedData = optionalBodyString(resource, 'associated_data');

  try {
    const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(apiV3Key, 'utf8'), Buffer.from(nonce, 'utf8'));
    if (associatedData) decipher.setAAD(Buffer.from(associatedData, 'utf8'));
    const encrypted = base64urlDecode(ciphertext);
    const authTag = encrypted.subarray(encrypted.length - 16);
    const cipherText = encrypted.subarray(0, encrypted.length - 16);
    decipher.setAuthTag(authTag);
    const buffer = Buffer.concat([decipher.update(cipherText), decipher.final()]);
    return safeJson(JSON.parse(buffer.toString('utf8')));
  } catch {
    throw new HttpError(400, 'WeChat Pay notification decrypt failed', 'PAYMENT_NOTIFICATION_INVALID');
  }
}

class WechatPayProvider implements PaymentProvider {
  readonly name = 'wechat_pay' as const;

  constructor(private readonly config: TenantPaymentProviderConfig) {}

  async createPayment(input: PaymentOrderInput): Promise<PaymentCreateResult> {
    const appId = requirePublicString(this.config, ['appId'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const mchId = requirePublicString(this.config, ['merchantId', 'mchId'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const merchantSerialNo = requirePublicString(this.config, ['merchantSerialNo'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const notifyUrl = requirePublicString(this.config, ['notifyUrl'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const endpoint = providerEndpoint(
      this.config,
      'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi',
      ['api.mch.weixin.qq.com'],
      'PAYMENT_ENDPOINT_NOT_ALLOWED',
    );
    const openId = input.openId || optionalPublicString(this.config, ['testOpenId']);
    if (!openId) {
      throw new HttpError(400, 'openId is required for WeChat JSAPI payment', 'WECHAT_OPENID_REQUIRED');
    }

    const payload = JSON.stringify({
      appid: appId,
      mchid: mchId,
      description: input.description.slice(0, 120),
      out_trade_no: input.orderNo,
      notify_url: notifyUrl,
      amount: {
        total: input.amountCents,
        currency: 'CNY',
      },
      payer: {
        openid: openId,
      },
    });
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonce = randomNonce();
    const url = new URL(endpoint);
    const message = ['POST', `${url.pathname}${url.search}`, timestamp, nonce, payload].join('\n') + '\n';
    const signature = rsaSignSha256(wechatPrivateKey(this.config), message);

    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        accept: 'application/json',
        authorization: `WECHATPAY2-SHA256-RSA2048 mchid="${mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${merchantSerialNo}"`,
        'content-type': 'application/json',
      },
      body: payload,
    });
    const raw = safeJson(await response.json().catch(() => ({})));
    if (!response.ok) {
      throw new HttpError(502, 'WeChat Pay create transaction failed', 'PAYMENT_PROVIDER_CREATE_FAILED');
    }

    const prepayId = typeof raw.prepay_id === 'string' ? raw.prepay_id : '';
    if (!prepayId) throw new HttpError(502, 'WeChat Pay response is missing prepay_id', 'PAYMENT_PROVIDER_RESPONSE_INVALID');
    const payTimestamp = Math.floor(Date.now() / 1000).toString();
    const payNonce = randomNonce();
    const packageValue = `prepay_id=${prepayId}`;
    const paySignMessage = [appId, payTimestamp, payNonce, packageValue].join('\n') + '\n';
    const paySign = rsaSignSha256(wechatPrivateKey(this.config), paySignMessage);

    return {
      provider: this.name,
      method: 'jsapi',
      providerTradeNo: prepayId,
      paymentParams: {
        appId,
        timeStamp: payTimestamp,
        nonceStr: payNonce,
        package: packageValue,
        signType: 'RSA',
        paySign,
      },
      raw: {
        prepayId,
      },
    };
  }

  async createRefund(input: PaymentRefundInput): Promise<PaymentRefundResult> {
    const mchId = requirePublicString(this.config, ['merchantId', 'mchId'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const merchantSerialNo = requirePublicString(this.config, ['merchantSerialNo'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const endpoint = providerEndpointForKeys(
      this.config,
      ['refundEndpoint', 'endpoint'],
      'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds',
      ['api.mch.weixin.qq.com'],
      'PAYMENT_ENDPOINT_NOT_ALLOWED',
    );
    const notifyUrl = input.notifyUrl || optionalPublicString(this.config, ['refundNotifyUrl', 'notifyUrl']);
    const payloadObject: Record<string, unknown> = {
      out_trade_no: input.orderNo,
      out_refund_no: input.refundNo,
      reason: (input.reason || 'refund').slice(0, 80),
      amount: {
        refund: input.amountCents,
        total: input.totalAmountCents,
        currency: 'CNY',
      },
    };
    if (input.providerTradeNo) {
      delete payloadObject.out_trade_no;
      payloadObject.transaction_id = input.providerTradeNo;
    }
    if (notifyUrl) payloadObject.notify_url = notifyUrl;

    const payload = JSON.stringify(payloadObject);
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonce = randomNonce();
    const url = new URL(endpoint);
    const message = ['POST', `${url.pathname}${url.search}`, timestamp, nonce, payload].join('\n') + '\n';
    const signature = rsaSignSha256(wechatPrivateKey(this.config), message);

    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        accept: 'application/json',
        authorization: `WECHATPAY2-SHA256-RSA2048 mchid="${mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${merchantSerialNo}"`,
        'content-type': 'application/json',
      },
      body: payload,
    });
    const raw = safeJson(await response.json().catch(() => ({})));
    if (!response.ok) {
      throw new HttpError(502, 'WeChat Pay refund request failed', 'PAYMENT_PROVIDER_REFUND_FAILED');
    }

    const statusText = typeof raw.status === 'string' ? raw.status.toUpperCase() : '';
    const status = wechatRefundStatus(statusText);
    const amount = objectValue(raw.amount);
    assertProviderRefundAmount(optionalIntegerAmount(amount.refund), input.amountCents);
    return {
      provider: this.name,
      status,
      providerRefundNo: typeof raw.refund_id === 'string' ? raw.refund_id : undefined,
      failureReason: status === 'failed' ? (statusText || 'WeChat refund failed') : undefined,
      raw,
    };
  }

  async queryRefund(input: PaymentRefundQueryInput): Promise<PaymentRefundResult> {
    const mchId = requirePublicString(this.config, ['merchantId', 'mchId'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const merchantSerialNo = requirePublicString(this.config, ['merchantSerialNo'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const endpoint = providerEndpointForKeys(
      this.config,
      ['refundQueryEndpoint', 'refundEndpoint'],
      'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds',
      ['api.mch.weixin.qq.com'],
      'PAYMENT_ENDPOINT_NOT_ALLOWED',
    );
    const url = new URL(`${endpoint.replace(/\/$/, '')}/${encodeURIComponent(input.refundNo)}`);
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonce = randomNonce();
    const message = ['GET', `${url.pathname}${url.search}`, timestamp, nonce, ''].join('\n') + '\n';
    const signature = rsaSignSha256(wechatPrivateKey(this.config), message);

    const response = await fetch(url.toString(), {
      method: 'GET',
      headers: {
        accept: 'application/json',
        authorization: `WECHATPAY2-SHA256-RSA2048 mchid="${mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${merchantSerialNo}"`,
      },
    });
    const raw = safeJson(await response.json().catch(() => ({})));
    if (!response.ok) {
      throw new HttpError(502, 'WeChat Pay refund query failed', 'PAYMENT_PROVIDER_REFUND_QUERY_FAILED');
    }

    const statusText = typeof raw.status === 'string' ? raw.status.toUpperCase() : '';
    const status = wechatRefundStatus(statusText);
    const amount = objectValue(raw.amount);
    assertProviderRefundAmount(optionalIntegerAmount(amount.refund), input.amountCents);
    return {
      provider: this.name,
      status,
      providerRefundNo: typeof raw.refund_id === 'string' ? raw.refund_id : input.providerRefundNo || undefined,
      failureReason: status === 'failed' ? (statusText || 'WeChat refund failed') : undefined,
      raw,
    };
  }

  async parseNotification(input: {
    headers: Record<string, string>;
    body: Record<string, unknown>;
    rawBody: string;
  }): Promise<PaymentNotificationResult> {
    verifyWechatNotification(this.config, input);
    const decrypted = decryptWechatNotificationResource(this.config, input.body);

    const tradeState = requiredBodyString(decrypted, 'trade_state', 'PAYMENT_NOTIFICATION_INVALID');
    if (tradeState !== 'SUCCESS') {
      throw new HttpError(409, `WeChat Pay trade state is ${tradeState}`, 'PAYMENT_NOT_PAID');
    }
    const orderNo = requiredBodyString(decrypted, 'out_trade_no', 'PAYMENT_NOTIFICATION_INVALID');
    const providerTradeNo = requiredBodyString(decrypted, 'transaction_id', 'PAYMENT_NOTIFICATION_INVALID');
    const amount = objectValue(decrypted.amount);
    const amountCents = Number(amount.total);
    if (!Number.isInteger(amountCents) || amountCents < 0) {
      throw new HttpError(400, 'WeChat Pay notification amount is invalid', 'PAYMENT_NOTIFICATION_INVALID');
    }

    return {
      provider: this.name,
      eventId: requiredBodyString(input.body, 'id', 'PAYMENT_NOTIFICATION_INVALID'),
      eventType: optionalBodyString(input.body, 'event_type') || 'TRANSACTION.SUCCESS',
      orderNo,
      providerTradeNo,
      amountCents,
      paidAt: optionalBodyString(decrypted, 'success_time') || undefined,
      raw: {
        notification: input.body,
        transaction: decrypted,
        headers: {
          serial: input.headers['wechatpay-serial'] || '',
          timestamp: input.headers['wechatpay-timestamp'] || '',
        },
      },
    };
  }

  async parseRefundNotification(input: {
    headers: Record<string, string>;
    body: Record<string, unknown>;
    rawBody: string;
  }): Promise<PaymentRefundNotificationResult> {
    verifyWechatNotification(this.config, input);
    const decrypted = decryptWechatNotificationResource(this.config, input.body);
    const statusText = requiredBodyString(decrypted, 'refund_status', 'PAYMENT_NOTIFICATION_INVALID').toUpperCase();
    const status = wechatRefundStatus(statusText);
    const orderNo = requiredBodyString(decrypted, 'out_trade_no', 'PAYMENT_NOTIFICATION_INVALID');
    const refundNo = requiredBodyString(decrypted, 'out_refund_no', 'PAYMENT_NOTIFICATION_INVALID');
    const amount = objectValue(decrypted.amount);
    const amountCents = Number(amount.refund ?? amount.payer_refund);
    if (!Number.isInteger(amountCents) || amountCents <= 0) {
      throw new HttpError(400, 'WeChat Pay refund notification amount is invalid', 'PAYMENT_NOTIFICATION_INVALID');
    }

    return {
      provider: this.name,
      eventId: requiredBodyString(input.body, 'id', 'PAYMENT_NOTIFICATION_INVALID'),
      eventType: optionalBodyString(input.body, 'event_type') || 'REFUND.SUCCESS',
      orderNo,
      refundNo,
      amountCents,
      status,
      providerRefundNo: optionalBodyString(decrypted, 'refund_id') || undefined,
      failureReason: status === 'failed'
        ? optionalBodyString(decrypted, 'refund_status') || 'WeChat refund failed'
        : undefined,
      notifiedAt: optionalBodyString(decrypted, 'success_time') || optionalBodyString(input.body, 'create_time') || undefined,
      raw: {
        notification: input.body,
        refund: decrypted,
        headers: {
          serial: input.headers['wechatpay-serial'] || '',
          timestamp: input.headers['wechatpay-timestamp'] || '',
        },
      },
    };
  }

}

class AlipayProvider implements PaymentProvider {
  readonly name = 'alipay' as const;

  constructor(private readonly config: TenantPaymentProviderConfig) {}

  async createPayment(input: PaymentOrderInput): Promise<PaymentCreateResult> {
    const appId = requirePublicString(this.config, ['appId'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const notifyUrl = requirePublicString(this.config, ['notifyUrl'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const returnUrl = input.returnUrl || optionalPublicString(this.config, ['returnUrl']);
    const quitUrl = input.quitUrl || optionalPublicString(this.config, ['quitUrl']);
    const gateway = providerEndpoint(
      this.config,
      'https://openapi.alipay.com/gateway.do',
      ['openapi.alipay.com'],
      'PAYMENT_ENDPOINT_NOT_ALLOWED',
    );
    const privateKey = normalizePem(requireSecretString(this.config, ['privateKey', 'appPrivateKey'], 'PAYMENT_SECRET_REQUIRED'), 'PRIVATE KEY');
    const bizContent: Record<string, unknown> = {
      out_trade_no: input.orderNo,
      total_amount: (input.amountCents / 100).toFixed(2),
      subject: input.description.slice(0, 120),
      product_code: 'QUICK_WAP_WAY',
    };
    if (quitUrl) bizContent.quit_url = quitUrl;
    const params: Record<string, string> = {
      app_id: appId,
      method: 'alipay.trade.wap.pay',
      charset: 'utf-8',
      sign_type: 'RSA2',
      timestamp: new Date().toISOString().replace('T', ' ').slice(0, 19),
      version: '1.0',
      notify_url: notifyUrl,
      biz_content: JSON.stringify(bizContent),
    };
    if (returnUrl) params.return_url = returnUrl;
    params.sign = rsaSignSha256(privateKey, canonicalForm(params));

    return {
      provider: this.name,
      method: 'wap',
      paymentParams: {
        gateway,
        method: 'GET',
        query: params,
        url: `${gateway}?${encodedForm(params)}`,
      },
      raw: {
        outTradeNo: input.orderNo,
      },
    };
  }

  async createRefund(input: PaymentRefundInput): Promise<PaymentRefundResult> {
    const appId = requirePublicString(this.config, ['appId'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const gateway = providerEndpointForKeys(
      this.config,
      ['refundEndpoint', 'endpoint'],
      'https://openapi.alipay.com/gateway.do',
      ['openapi.alipay.com'],
      'PAYMENT_ENDPOINT_NOT_ALLOWED',
    );
    const privateKey = normalizePem(requireSecretString(this.config, ['privateKey', 'appPrivateKey'], 'PAYMENT_SECRET_REQUIRED'), 'PRIVATE KEY');
    const bizContent: Record<string, unknown> = {
      out_trade_no: input.orderNo,
      out_request_no: input.refundNo,
      refund_amount: (input.amountCents / 100).toFixed(2),
      refund_reason: (input.reason || 'refund').slice(0, 256),
    };
    if (input.providerTradeNo) {
      delete bizContent.out_trade_no;
      bizContent.trade_no = input.providerTradeNo;
    }
    const params: Record<string, string> = {
      app_id: appId,
      method: 'alipay.trade.refund',
      charset: 'utf-8',
      sign_type: 'RSA2',
      timestamp: new Date().toISOString().replace('T', ' ').slice(0, 19),
      version: '1.0',
      biz_content: JSON.stringify(bizContent),
    };
    params.sign = rsaSignSha256(privateKey, canonicalForm(params));

    const response = await fetch(gateway, {
      method: 'POST',
      headers: {
        accept: 'application/json',
        'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
      },
      body: encodedForm(params),
    });
    const raw = safeJson(await response.json().catch(() => ({})));
    if (!response.ok) {
      throw new HttpError(502, 'Alipay refund request failed', 'PAYMENT_PROVIDER_REFUND_FAILED');
    }

    const responseBody = objectValue(raw.alipay_trade_refund_response);
    const code = typeof responseBody.code === 'string' ? responseBody.code : '';
    if (code !== '10000') {
      throw new HttpError(502, 'Alipay refund was rejected', 'PAYMENT_PROVIDER_REFUND_REJECTED');
    }
    const status = alipayRefundStatus(responseBody);
    assertProviderRefundAmount(optionalYuanToCents(responseBody.refund_fee), input.amountCents);

    return {
      provider: this.name,
      status,
      providerRefundNo:
        (typeof responseBody.trade_no === 'string' && responseBody.trade_no)
        || (typeof responseBody.out_request_no === 'string' && responseBody.out_request_no)
        || undefined,
      failureReason: status === 'failed'
        ? (typeof responseBody.sub_msg === 'string' && responseBody.sub_msg) || 'Alipay refund failed'
        : undefined,
      raw,
    };
  }

  async queryRefund(input: PaymentRefundQueryInput): Promise<PaymentRefundResult> {
    const appId = requirePublicString(this.config, ['appId'], 'PAYMENT_PUBLIC_CONFIG_REQUIRED');
    const gateway = providerEndpointForKeys(
      this.config,
      ['refundQueryEndpoint', 'refundEndpoint', 'endpoint'],
      'https://openapi.alipay.com/gateway.do',
      ['openapi.alipay.com'],
      'PAYMENT_ENDPOINT_NOT_ALLOWED',
    );
    const privateKey = normalizePem(requireSecretString(this.config, ['privateKey', 'appPrivateKey'], 'PAYMENT_SECRET_REQUIRED'), 'PRIVATE KEY');
    const bizContent: Record<string, unknown> = {
      out_trade_no: input.orderNo,
      out_request_no: input.refundNo,
    };
    if (input.providerTradeNo) {
      delete bizContent.out_trade_no;
      bizContent.trade_no = input.providerTradeNo;
    }
    const params: Record<string, string> = {
      app_id: appId,
      method: 'alipay.trade.fastpay.refund.query',
      charset: 'utf-8',
      sign_type: 'RSA2',
      timestamp: new Date().toISOString().replace('T', ' ').slice(0, 19),
      version: '1.0',
      biz_content: JSON.stringify(bizContent),
    };
    params.sign = rsaSignSha256(privateKey, canonicalForm(params));

    const response = await fetch(gateway, {
      method: 'POST',
      headers: {
        accept: 'application/json',
        'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
      },
      body: encodedForm(params),
    });
    const raw = safeJson(await response.json().catch(() => ({})));
    if (!response.ok) {
      throw new HttpError(502, 'Alipay refund query failed', 'PAYMENT_PROVIDER_REFUND_QUERY_FAILED');
    }

    const responseBody = objectValue(raw.alipay_trade_fastpay_refund_query_response);
    const code = typeof responseBody.code === 'string' ? responseBody.code : '';
    if (code !== '10000') {
      throw new HttpError(502, 'Alipay refund query was rejected', 'PAYMENT_PROVIDER_REFUND_QUERY_REJECTED');
    }
    const status = alipayRefundStatus(responseBody);
    assertProviderRefundAmount(optionalYuanToCents(responseBody.refund_amount), input.amountCents);
    return {
      provider: this.name,
      status,
      providerRefundNo:
        (typeof responseBody.trade_no === 'string' && responseBody.trade_no)
        || (typeof responseBody.out_request_no === 'string' && responseBody.out_request_no)
        || input.providerRefundNo
        || undefined,
      failureReason: status === 'failed'
        ? (typeof responseBody.sub_msg === 'string' && responseBody.sub_msg) || 'Alipay refund failed'
        : undefined,
      raw,
    };
  }

  async parseNotification(input: {
    headers: Record<string, string>;
    body: Record<string, unknown>;
    rawBody: string;
  }): Promise<PaymentNotificationResult> {
    const alipayPublicKey = normalizePem(
      requireSecretString(this.config, ['alipayPublicKey', 'publicKey'], 'PAYMENT_SECRET_REQUIRED'),
      'PUBLIC KEY',
    );
    const params: Record<string, string> = {};
    for (const [key, value] of Object.entries(input.body)) {
      if (typeof value === 'string') params[key] = value;
    }
    const sign = params.sign || '';
    const signType = params.sign_type || 'RSA2';
    delete params.sign;
    delete params.sign_type;
    if (signType !== 'RSA2') throw new HttpError(400, 'Alipay notification must use RSA2', 'PAYMENT_SIGNATURE_INVALID');
    if (!rsaVerifySha256(alipayPublicKey, canonicalForm(params), sign)) {
      throw new HttpError(400, 'Alipay notification signature invalid', 'PAYMENT_SIGNATURE_INVALID');
    }
    if (!['TRADE_SUCCESS', 'TRADE_FINISHED'].includes(params.trade_status || '')) {
      throw new HttpError(409, `Alipay trade status is ${params.trade_status || 'unknown'}`, 'PAYMENT_NOT_PAID');
    }

    const totalAmount = Number(params.total_amount || params.receipt_amount);
    const amountCents = Math.round(totalAmount * 100);
    if (!Number.isInteger(amountCents) || amountCents < 0) {
      throw new HttpError(400, 'Alipay notification amount is invalid', 'PAYMENT_NOTIFICATION_INVALID');
    }

    return {
      provider: this.name,
      eventId: params.notify_id || `${params.trade_no}:${params.trade_status}`,
      eventType: params.trade_status || 'TRADE_SUCCESS',
      orderNo: params.out_trade_no,
      providerTradeNo: params.trade_no,
      amountCents,
      paidAt: params.gmt_payment || params.notify_time || undefined,
      raw: {
        notification: input.body,
      },
    };
  }

  async parseRefundNotification(input: {
    headers: Record<string, string>;
    body: Record<string, unknown>;
    rawBody: string;
  }): Promise<PaymentRefundNotificationResult> {
    const alipayPublicKey = normalizePem(
      requireSecretString(this.config, ['alipayPublicKey', 'publicKey'], 'PAYMENT_SECRET_REQUIRED'),
      'PUBLIC KEY',
    );
    const params: Record<string, string> = {};
    for (const [key, value] of Object.entries(input.body)) {
      if (typeof value === 'string') params[key] = value;
    }
    const sign = params.sign || '';
    const signType = params.sign_type || 'RSA2';
    delete params.sign;
    delete params.sign_type;
    if (signType !== 'RSA2') throw new HttpError(400, 'Alipay notification must use RSA2', 'PAYMENT_SIGNATURE_INVALID');
    if (!rsaVerifySha256(alipayPublicKey, canonicalForm(params), sign)) {
      throw new HttpError(400, 'Alipay notification signature invalid', 'PAYMENT_SIGNATURE_INVALID');
    }

    const orderNo = params.out_trade_no;
    const refundNo = params.out_biz_no || params.out_request_no || params.refund_no;
    if (!orderNo || !refundNo) throw new HttpError(400, 'Alipay refund notification is missing refund identity', 'PAYMENT_NOTIFICATION_INVALID');
    const amountCents = optionalYuanToCents(params.refund_fee || params.refund_amount);
    if (!amountCents || amountCents <= 0) {
      throw new HttpError(400, 'Alipay refund notification amount is invalid', 'PAYMENT_NOTIFICATION_INVALID');
    }
    const status = alipayRefundStatus(params);

    return {
      provider: this.name,
      eventId: params.notify_id || `${params.trade_no || orderNo}:${refundNo}:${params.notify_time || params.gmt_refund || ''}`,
      eventType: params.notify_type || params.trade_status || 'REFUND.NOTIFY',
      orderNo,
      refundNo,
      amountCents,
      status,
      providerRefundNo: params.trade_no || refundNo,
      failureReason: status === 'failed'
        ? params.sub_msg || params.refund_status || 'Alipay refund failed'
        : undefined,
      notifiedAt: params.gmt_refund || params.notify_time || undefined,
      raw: {
        notification: input.body,
      },
    };
  }
}

class ManualPaymentProvider implements PaymentProvider {
  readonly name = 'manual' as const;

  async createPayment(input: PaymentOrderInput): Promise<PaymentCreateResult> {
    return {
      provider: this.name,
      method: 'manual',
      paymentParams: {
        orderNo: input.orderNo,
        amountCents: input.amountCents,
      },
      raw: {},
    };
  }

  async createRefund(): Promise<PaymentRefundResult> {
    throw new HttpError(501, 'Manual provider does not support online refunds', 'PAYMENT_PROVIDER_REFUND_NOT_SUPPORTED');
  }

  async queryRefund(): Promise<PaymentRefundResult> {
    throw new HttpError(501, 'Manual provider does not support online refund query', 'PAYMENT_PROVIDER_REFUND_NOT_SUPPORTED');
  }

  async parseNotification(): Promise<PaymentNotificationResult> {
    throw new HttpError(501, 'Manual provider does not support webhook notifications', 'PAYMENT_PROVIDER_NOT_SUPPORTED');
  }

  async parseRefundNotification(): Promise<PaymentRefundNotificationResult> {
    throw new HttpError(501, 'Manual provider does not support refund webhook notifications', 'PAYMENT_PROVIDER_REFUND_NOT_SUPPORTED');
  }
}

export function normalizePaymentProvider(value: string): PaymentProviderName {
  const normalized = value.toLowerCase().replace(/[-\s]/g, '_');
  if (['wechat', 'wechatpay', 'wxpay', 'wx_pay', 'wechat_pay'].includes(normalized)) return 'wechat_pay';
  if (['alipay', 'ali_pay'].includes(normalized)) return 'alipay';
  return 'manual';
}

export function paymentProviderAliases(value: string) {
  const provider = normalizePaymentProvider(value);
  if (provider === 'wechat_pay') return ['wechat_pay', 'wechat-pay', 'wechatpay', 'wxpay'];
  if (provider === 'alipay') return ['alipay', 'ali_pay'];
  return ['manual'];
}

export function createPaymentProvider(
  name: string,
  config?: TenantPaymentProviderConfig | null,
): PaymentProvider {
  const provider = normalizePaymentProvider(name);
  if (provider === 'wechat_pay') {
    if (!config) throw new HttpError(503, 'WeChat Pay provider is not configured', 'PAYMENT_PROVIDER_NOT_CONFIGURED');
    return new WechatPayProvider(config);
  }
  if (provider === 'alipay') {
    if (!config) throw new HttpError(503, 'Alipay provider is not configured', 'PAYMENT_PROVIDER_NOT_CONFIGURED');
    return new AlipayProvider(config);
  }
  return new ManualPaymentProvider();
}

export function allowsLocalMockPayment(config: TenantPaymentProviderConfig | null) {
  return config ? optionalPublicBoolean(config, 'allowLocalMock', false) : false;
}
