<?php
/**
 * 微信支付 API V3 通用请求封装。
 *
 * 提供账号配置加载、请求签名、敏感字段加解密、HTTP 请求、
 * 应答验签和错误解析，具体业务接口只负责组装 path 和 body。
 */
class WxPayV3Api
{
	private $accountTag = null;
	private $config = [];
	private $merchantPrivateKey = null;
	private $wechatPayPublicKey = null;
	private $lastHttpStatus = 0;
	private $lastResponseHeaders = [];

	public function __construct($accountTag)
	{
		$this->accountTag = $accountTag;
		$this->config = $this->loadConfig($accountTag);
		$this->merchantPrivateKey = openssl_pkey_get_private(file_get_contents($this->config['SSLKEY_PATH']));
		if($this->merchantPrivateKey === false)
			throw new WxPayException('微信商户API私钥读取失败');

		$this->wechatPayPublicKey = openssl_pkey_get_public(file_get_contents($this->config['WECHATPAY_PUBLIC_KEY_PATH']));
		if($this->wechatPayPublicKey === false)
			throw new WxPayException('微信支付公钥读取失败');
	}


	/**
	 * 获取当前账号的 AppID。
	 */
	public function getAppid()
	{
		return $this->config['APPID'];
	}


	/**
	 * 获取最近一次请求的 HTTP 状态码。
	 */
	public function getLastHttpStatus()
	{
		return $this->lastHttpStatus;
	}


	/**
	 * 获取最近一次请求的应答头。
	 */
	public function getLastResponseHeaders()
	{
		return $this->lastResponseHeaders;
	}


	/**
	 * 使用微信支付公钥加密敏感字段。
	 */
	public function encryptSensitive($plainText)
	{
		if($plainText === null || $plainText === '') return '';
		$encrypted = '';
		if(!openssl_public_encrypt($plainText, $encrypted, $this->wechatPayPublicKey, OPENSSL_PKCS1_OAEP_PADDING))
			throw new WxPayException('微信支付敏感字段加密失败');
		return base64_encode($encrypted);
	}


	/**
	 * 使用商户 API 私钥解密敏感字段。
	 */
	public function decryptSensitive($cipherText)
	{
		if($cipherText === null || $cipherText === '') return '';
		$encrypted = base64_decode($cipherText, true);
		if($encrypted === false)
			throw new WxPayException('微信支付敏感字段不是有效的Base64');
		$plainText = '';
		if(!openssl_private_decrypt($encrypted, $plainText, $this->merchantPrivateKey, OPENSSL_PKCS1_OAEP_PADDING))
			throw new WxPayException('微信支付敏感字段解密失败');
		return $plainText;
	}


	/**
	 * 发起 GET 请求。
	 */
	public function get($path, $headers = [])
	{
		return $this->request('GET', $path, null, $headers);
	}


	/**
	 * 发起 POST JSON 请求。
	 */
	public function post($path, $body = [], $headers = [])
	{
		return $this->request('POST', $path, $body, $headers);
	}


	/**
	 * 发起 PUT JSON 请求。
	 */
	public function put($path, $body = [], $headers = [])
	{
		return $this->request('PUT', $path, $body, $headers);
	}


	/**
	 * 发起 PATCH JSON 请求。
	 */
	public function patch($path, $body = [], $headers = [])
	{
		return $this->request('PATCH', $path, $body, $headers);
	}


	/**
	 * 发起 DELETE 请求。
	 */
	public function delete($path, $body = null, $headers = [])
	{
		return $this->request('DELETE', $path, $body, $headers);
	}


	/**
	 * 发起微信支付 API V3 请求。
	 *
	 * $body 传数组时自动编码为 JSON，传字符串时按原文参与签名并发送。
	 */
	public function request($method, $path, $body = null, $headers = [])
	{
		$method = strtoupper($method);
		if(empty($path) || substr($path, 0, 1) != '/')
			throw new WxPayException('微信支付API V3请求路径必须以/开头');

		$requestBody = $this->buildRequestBody($body);
		$authorization = $this->buildAuthorization($method, $path, $requestBody);
		$requestHeaders = [
			'Authorization'=>$authorization,
			'Accept'=>'application/json',
			'User-Agent'=>'Yiparts-API/1.0',
			'Wechatpay-Serial'=>$this->config['WECHATPAY_PUBLIC_KEY_ID']
		];
		if($body !== null) $requestHeaders['Content-Type'] = 'application/json';
		foreach($headers as $name=>$value)
		{
			if(is_int($name))
			{
				$pos = strpos($value, ':');
				if($pos === false) continue;
				$name = trim(substr($value, 0, $pos));
				$value = trim(substr($value, $pos + 1));
			}
			$requestHeaders[$name] = $value;
		}

		$curlHeaders = [];
		foreach($requestHeaders as $name=>$value)
		{
			$curlHeaders[] = $name.': '.$value;
		}

		$this->lastResponseHeaders = [];
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, 'https://api.mch.weixin.qq.com'.$path);
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
		curl_setopt($ch, CURLOPT_TIMEOUT, 30);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header){
			$len = strlen($header);
			$pos = strpos($header, ':');
			if($pos !== false)
			{
				$name = strtolower(trim(substr($header, 0, $pos)));
				$this->lastResponseHeaders[$name] = trim(substr($header, $pos + 1));
			}
			return $len;
		});
		if($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody);

		$responseBody = curl_exec($ch);
		if($responseBody === false)
		{
			$curlError = curl_error($ch);
			curl_close($ch);
			throw new WxPayException('微信支付API V3请求失败:'.$curlError);
		}
		$this->lastHttpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		curl_close($ch);

		if($this->lastHttpStatus >= 200 && $this->lastHttpStatus < 300)
		{
			$this->verifyResponse($responseBody);
		}

		if($responseBody === '' && $this->lastHttpStatus >= 200 && $this->lastHttpStatus < 300)
			return [];

		$result = json_decode($responseBody, true);
		if(!is_array($result))
			$result = ['code'=>'SYSTEM', 'message'=>'微信支付返回数据格式错误'];
		if($this->lastHttpStatus < 200 || $this->lastHttpStatus >= 300)
			$result['http_status'] = $this->lastHttpStatus;
		return $result;
	}


	/**
	 * 加载 API V3 所需配置。
	 */
	private function loadConfig($accountTag)
	{
		if(empty(mvc::$cfg['PAY']['WX'][$accountTag]))
			throw new WxPayException('API支付账号:'.$accountTag.'不存在');

		$config = mvc::$cfg['PAY']['WX'][$accountTag];
		$mustArr = ['APPID', 'MCHID', 'SSLCERT_PATH', 'SSLKEY_PATH', 'WECHATPAY_PUBLIC_KEY_ID', 'WECHATPAY_PUBLIC_KEY_PATH'];
		foreach($mustArr as $field)
		{
			if(empty($config[$field]))
				throw new WxPayException('微信支付API V3配置项'.$field.'不存在');
		}
		if(!is_file($config['SSLCERT_PATH']))
			throw new WxPayException('微信商户API证书文件不存在');
		if(!is_file($config['SSLKEY_PATH']))
			throw new WxPayException('微信商户API私钥文件不存在');
		if(!is_file($config['WECHATPAY_PUBLIC_KEY_PATH']))
			throw new WxPayException('微信支付公钥文件不存在');

		if(empty($config['MCH_CERT_SERIAL_NO']))
		{
			$certData = openssl_x509_parse(file_get_contents($config['SSLCERT_PATH']));
			if(empty($certData['serialNumberHex']))
				throw new WxPayException('无法读取微信商户API证书序列号');
			$config['MCH_CERT_SERIAL_NO'] = strtoupper($certData['serialNumberHex']);
		}
		return $config;
	}


	/**
	 * 将请求体转换为参与签名和发送的字符串。
	 */
	private function buildRequestBody($body)
	{
		if($body === null) return '';
		if(is_string($body)) return $body;
		$requestBody = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
		if($requestBody === false)
			throw new WxPayException('微信支付API V3请求参数JSON编码失败');
		return $requestBody;
	}


	/**
	 * 构造 Authorization 请求头。
	 */
	private function buildAuthorization($method, $path, $requestBody)
	{
		$timestamp = time();
		$nonce = bin2hex(random_bytes(16));
		$message = $method."\n".$path."\n".$timestamp."\n".$nonce."\n".$requestBody."\n";
		$signature = '';
		if(!openssl_sign($message, $signature, $this->merchantPrivateKey, OPENSSL_ALGO_SHA256))
			throw new WxPayException('微信支付API V3请求签名失败');

		return sprintf(
			'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
			$this->config['MCHID'],
			$nonce,
			$timestamp,
			$this->config['MCH_CERT_SERIAL_NO'],
			base64_encode($signature)
		);
	}


	/**
	 * 验证微信支付成功应答签名。
	 */
	private function verifyResponse($responseBody)
	{
		$signFields = ['wechatpay-timestamp', 'wechatpay-nonce', 'wechatpay-signature', 'wechatpay-serial'];
		foreach($signFields as $field)
		{
			if(empty($this->lastResponseHeaders[$field]))
				throw new WxPayException('微信支付API V3应答验签字段缺失:'.$field);
		}
		if($this->lastResponseHeaders['wechatpay-serial'] != $this->config['WECHATPAY_PUBLIC_KEY_ID'])
			throw new WxPayException('微信支付API V3应答公钥ID不匹配');

		$verifyMessage = $this->lastResponseHeaders['wechatpay-timestamp']."\n".
			$this->lastResponseHeaders['wechatpay-nonce']."\n".$responseBody."\n";
		$signature = base64_decode($this->lastResponseHeaders['wechatpay-signature'], true);
		if($signature === false)
			throw new WxPayException('微信支付API V3应答签名不是有效的Base64');
		$verify = openssl_verify($verifyMessage, $signature, $this->wechatPayPublicKey, OPENSSL_ALGO_SHA256);
		if($verify !== 1)
			throw new WxPayException('微信支付API V3应答签名验证失败');
	}
}
