<?php
// +----------------------------------------------------------------------
// | onezan-admin 快速开发的管理平台
// +----------------------------------------------------------------------
// | 官方网址：https://www.onezan.cn
// +----------------------------------------------------------------------
// | onezan团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: 543885307@qq.com
// +----------------------------------------------------------------------
namespace app\service\domain\pay\gateway;

use app\model\UserChannelAccount;
use app\service\domain\pay\CorePayConfigService;
use think\Request;
use Yansongda\Pay\Pay;


/**
 * 支付网关-微信支付网关
 * @package app\service\domain\pay\gateway
 */
class WechatGateway extends BaseGateway
{
    /**
     * 支付网关配置服务
     * @var CorePayConfigService
     */
    private CorePayConfigService $payConfig;

    /**
     * 构造函数
     * 初始化支付网关配置服务
     * @param CorePayConfigService $payConfig
     */
    public function __construct(CorePayConfigService $payConfig)
    {
        $this->payConfig = $payConfig;
    }


     /**
      * 支付网关提供方
      * @return string
      */
    public function provider(): string
    {
        return 'wechatpay';
    }


     /**
      * 支付网关名称
      * @return string
      */
    public function name(): string
    {
        return 'wechatpay';
    }


     /**
      * 支付网关是否支持终端
      * @param string $terminal 终端
      * @return bool
      */
    public function supportsTerminal(string $terminal): bool
    {
        return in_array($terminal, ['pc', 'h5', 'wechat', 'weapp', 'app'], true);
    }


     /**
      * 支付网关是否可分配
      * @return bool
      */
    public function isAllocatable(): bool
    {
        return true;
    }


     /**
      * 支付网关成功响应
      * @return array
      */
    public function successResponse(): array
    {
        return [
            'body' => (string)Pay::wechat()->success()->getBody(),
            'headers' => ['Content-Type' => 'application/json'],
        ];
    }


     /**
      * 支付网关创建支付
      * @param array $payment 支付信息
      * @param array $context 上下文
      * @return array
      */
    public function create(array $payment, array $order, string $terminal, array $context): array
    {
        $config = $this->buildGatewayConfig($terminal);
        $payload = [
            'out_trade_no' => (string)($payment['payment_no'] ?? ''),
            'description' => $this->buildSubject($payment, $order),
            'amount' => ['total' => $this->toFen($payment['amount'] ?? 0)],
        ];

        if ($this->isTestMode()) {
            return $this->testModeCreateResponse($payment, $terminal);
        }

        Pay::config([
            'wechat' => ['default' => $config],
            'logger' => ['enable' => false],
        ]);

        return $this->dispatchByTerminal($payload, $payment, $terminal, $context);
    }


     /**
      * 支付网关通知
      * @param Request $request 请求
      * @param string $terminal 终端
      * @return array
      */
    public function notify(Request $request, string $terminal): array
    {
        $terminal = $this->normalizeTerminal($terminal);
        $config = $this->buildNotifyConfig($terminal);

        Pay::config([
            'wechat' => ['default' => $config],
            'logger' => ['enable' => false],
        ]);

        $data = Pay::wechat()->callback([
            'body' => $request->getContent(),
            'headers' => $request->header(),
        ]);

        $tradeState = strtoupper((string)$data->get('trade_state', ''));
        if (!in_array($tradeState, ['SUCCESS', 'NOTPAY'], true)) {
            throw new \RuntimeException('');
        }

        return [
            'code' => $tradeState === 'SUCCESS' ? 0 : 1,
            'payment_no' => (string)$data->get('out_trade_no', ''),
            'trade_no' => (string)$data->get('transaction_id', ''),
            'amount' => $this->toYuan((int)$data->get('total_fee', 0)),
            'raw' => $data->all(),
        ];
    }


     /**
      * 支付网关退款
      * @param array $refund 退款信息
      * @param array $payment 支付信息
      * @return array
      */
    public function refund(array $refund, array $payment): array
    {
        if ($this->isTestMode()) {
            return $this->testModeRefundResponse($refund, $payment);
        }

        $terminal = $this->normalizeTerminal((string)($payment['terminal'] ?? ''));
        $config = $this->buildGatewayConfig($terminal);

        Pay::config([
            'wechat' => ['default' => $config],
            'logger' => ['enable' => false],
        ]);

        $action = $this->refundAction($terminal);
        $result = Pay::wechat()->refund([
            '_action' => $action,
            'out_trade_no' => (string)($payment['payment_no'] ?? ''),
            'out_refund_no' => (string)($refund['refund_no'] ?? ''),
            'reason' => (string)($refund['reason'] ?? ''),
            'notify_url' => (string)($config['refund_notify_url'] ?? $config['notify_url'] ?? ''),
            'amount' => [
                'refund' => $this->toFen($refund['amount'] ?? 0),
                'total' => $this->toFen($payment['amount'] ?? 0),
                'currency' => 'CNY',
            ],
        ]);

        $status = strtoupper((string)$result->get('status', ''));
        $isSuccess = $status === 'SUCCESS';

        return [
            'code' => 0,
            'msg' => $isSuccess ? '' : '',
            'status' => $isSuccess ? 3 : 2,
            'remark' => $isSuccess ? '' : '',
            'extra' => ['refund_response' => $result->all()],
        ];
    }


     /**
      * 支付网关退款通知
      * @param Request $request 请求
      * @param string $terminal 终端
      * @return array
      */
    public function refundNotify(Request $request, string $terminal): array
    {
        $config = $this->buildNotifyConfig($terminal);

        Pay::config([
            'wechat' => ['default' => $config],
            'logger' => ['enable' => false],
        ]);

        $data = Pay::wechat()->callback([
            'body' => $request->getContent(),
            'headers' => $request->header(),
        ]);

        $refundStatus = strtoupper((string)$data->get('refund_status', ''));
        return [
            'code' => 0,
            'refund_no' => (string)$data->get('out_refund_no', ''),
            'status' => $refundStatus,
            'raw' => $data->all(),
        ];
    }


     /**
      * 支付网关构建支付配置
      * @param string $terminal 终端
      * @return array
      */
    public function buildGatewayConfig(string $terminal): array
    {
        $channel = $this->terminalChannel($terminal);
        $config = $this->payConfig->getProviderConfig($channel, 'wechatpay');

        if ($this->isTestMode()) {
            $notifyUrl = (string)($config['notify_url'] ?? '');
            return [
                'mch_id' => 'TEST_MCH_000000',
                'mch_secret_key' => 'test_secret_key_32_bytes____',
                'mch_secret_cert' => '',
                'mch_public_cert_path' => '',
                'notify_url' => $notifyUrl,
                'refund_notify_url' => (string)($config['refund_notify_url'] ?? $notifyUrl),
                'mp_app_id' => 'wx_test_app_id',
                'mini_app_id' => 'wx_test_app_id',
                'app_id' => 'wx_test_app_id',
                'h5_app_id' => 'wx_test_app_id',
                '_test_mode' => true,
            ];
        }


        foreach (['app_id', 'mch_id', 'v3_secret_key', 'private_key', 'merchant_cert'] as $field) {
            if (empty($config[$field])) {
                throw new \RuntimeException('' . $field);
            }
        }

        $appId = (string)$config['app_id'];

        return [
            'mch_id' => (string)$config['mch_id'],
            'mch_secret_key' => (string)$config['v3_secret_key'],
            'mch_secret_cert' => (string)$config['merchant_cert'],
            'mch_public_cert_path' => $this->certPath($appId, $terminal, 'public_' . $terminal . '.pem', (string)$config['private_key']),
            'notify_url' => (string)($config['notify_url'] ?? ''),
            'refund_notify_url' => (string)($config['refund_notify_url'] ?? $config['notify_url'] ?? ''),
            'mp_app_id' => $appId,
            'mini_app_id' => $appId,
            'app_id' => $appId,
            'h5_app_id' => $appId,
        ];
    }


     /**
      * 支付网关构建通知配置
      * @param string $terminal 终端
      * @return array
      */
    public function buildNotifyConfig(string $terminal): array
    {
        return $this->buildGatewayConfig($terminal);
    }

     /**
      * 支付网关根据终端分发支付
      * @param array $payload 支付参数
      * @param array $payment 支付信息
      * @param string $terminal 终端
      * @param array $context 上下文
      * @return array
      */
    private function dispatchByTerminal(array $payload, array $payment, string $terminal, array $context): array
    {
        if ($terminal === 'pc') {
            $result = Pay::wechat()->scan($payload);
            return [
                'action' => 'qrcode',
                'message' => '',
                'code_url' => (string)$result->get('code_url', ''),
                'params' => $result->all(),
            ];
        }

        if ($terminal === 'h5') {
            $payload['scene_info'] = [
                'payer_client_ip' => $this->resolveClientIp($context),
                'h5_info' => ['type' => 'Wap'],
            ];
            $result = Pay::wechat()->h5($payload);
            return [
                'action' => 'redirect',
                'message' => '',
                'h5_url' => (string)$result->get('h5_url', ''),
                'params' => $result->all(),
            ];
        }

        $userId = (int)($context['user_id'] ?? 0);

        if ($terminal === 'wechat') {
            $payload['payer'] = ['openid' => $this->resolveWechatOpenid($userId, 'wechat')];
            $result = Pay::wechat()->mp($payload);
            return ['action' => 'jsapi', 'message' => '', 'params' => $result->all()];
        }

        if ($terminal === 'weapp') {
            $payload['payer'] = ['openid' => $this->resolveWechatOpenid($userId, 'weapp')];
            $result = Pay::wechat()->mini($payload);
            return ['action' => 'mini', 'message' => '', 'params' => $result->all()];
        }

        if ($terminal === 'app') {
            $result = Pay::wechat()->app($payload);
            return ['action' => 'app', 'message' => '', 'params' => $result->all()];
        }

        throw new \RuntimeException('');
    }

     /**
      * 支付网关根据用户ID和渠道获取微信openid
      * @param int $userId 用户ID
      * @param string $channel 渠道
      * @return string
      */
    private function resolveWechatOpenid(int $userId, string $channel): string
    {
        $account = UserChannelAccount::where([
            'user_id' => $userId,
            'channel' => $channel,
        ])->find();

        if (!$account || empty($account->openid)) {
            throw new \RuntimeException('' . $channel);
        }

        return (string)$account->openid;
    }

     /**
      * 支付网关根据上下文获取客户端IP
      * @param array $context 上下文
      * @return string
      */
    private function resolveClientIp(array $context): string
    {
        return (string)($context['client_ip'] ?? '127.0.0.1');
    }

     /**
      * 支付网关根据终端获取渠道
      * @param string $terminal 终端
      * @return string
      */
    private function terminalChannel(string $terminal): string
    {
        return $terminal === 'pc' ? 'h5' : $terminal;
    }

     /**
      * 支付网关根据终端获取证书路径
      * @param string $appId 应用ID
      * @param string $terminal 终端
      * @param string $filename 文件名
      * @param string $content 内容
      * @return string
      */
    private function certPath(string $appId, string $terminal, string $filename, string $content): string
    {
        $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'onezan_certs' . DIRECTORY_SEPARATOR . $appId . DIRECTORY_SEPARATOR . $terminal;
        if (!is_dir($dir)) {
            mkdir($dir, 0700, true);
        }
        $path = $dir . DIRECTORY_SEPARATOR . $filename;
        if (!file_exists($path) && !empty($content)) {
            file_put_contents($path, $content);
        }
        return $path;
    }

     /**
      * 支付网关根据终端获取退款操作
      * @param string $terminal 终端
      * @return string
      */
    private function refundAction(string $terminal): string
    {
        return $terminal === 'h5' ? 'out_trade_no' : 'out_refund_no';
    }
}