<?php

declare(strict_types=1);

namespace app\common\library\finance;

use InvalidArgumentException;
use RuntimeException;
use Webman\Http\Request;

/**
 * DDPay 支付网关接入（基于文档 v1.1.3/1.1.x）：
 * - MD5 签名：key-value 按 ASCII 升序拼接 + 追加 &key=API_SECRET
 * - 发送 HTTPS POST（application/json）
 * - 支持入金（Deposit）与回调通知（Webhook）
 *
 * 注意：生产环境需在 config/app.php 或环境变量中配置 DDPAY_*。
 */
final class DDPayGateway
{
    private const SIGNATURE_FIELD = 'signature';
    private const SECRET_KEY_PARAM = 'key';

    /**
     * 入金/出金回调 URL 使用的公网根地址（无尾部斜杠）。
     * 优先 `DDPAY_PUBLIC_BASE_URL`（见 config/app.php）；未配置时按请求头推导（生产务必配置 HTTPS 公网地址，与 DDPay 文档一致）。
     */
    public static function publicBaseUrlForCallbacks(?Request $request = null): string
    {
        $cfg = config('app.ddpay_public_base_url', '');
        if (is_string($cfg) && trim($cfg) !== '') {
            return rtrim(trim($cfg), '/');
        }
        if ($request === null) {
            return '';
        }
        $proto = strtolower((string) $request->header('x-forwarded-proto', ''));
        $https = $proto === 'https' || strtolower((string) $request->header('x-forwarded-ssl', '')) === 'on';
        $scheme = $https ? 'https' : 'http';
        $host = trim((string) $request->header('host', ''));
        if ($host === '') {
            $host = trim((string) ($request->header('x-forwarded-host', '')));
        }
        if ($host === '') {
            $host = '127.0.0.1:8787';
        }

        return $scheme . '://' . $host;
    }

    /**
     * 是否已配置 DDPay 入金所需项（未配置时不应调用三方接口）
     */
    public static function isConfigured(): bool
    {
        $envMap = [
            'ddpay_client_id'        => 'DDPAY_CLIENT_ID',
            'ddpay_identifier'       => 'DDPAY_IDENTIFIER',
            'ddpay_api_secret'       => 'DDPAY_API_SECRET',
            'ddpay_deposit_init_url' => 'DDPAY_DEPOSIT_INIT_URL',
        ];
        foreach ($envMap as $cfgKey => $envKey) {
            $v = getenv($envKey);
            if (!is_string($v) || trim($v) === '') {
                $cfg = config('app.' . $cfgKey, '');
                if (!is_string($cfg) || trim($cfg) === '') {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @return array<string, mixed>
     */
    public static function depositInitiation(array $params): array
    {
        $endpoint = self::requireConfig('ddpay_deposit_init_url');
        $apiSecret = self::requireConfig('ddpay_api_secret');

        $req = $params;
        if (isset($req[self::SIGNATURE_FIELD])) {
            unset($req[self::SIGNATURE_FIELD]);
        }

        $req[self::SIGNATURE_FIELD] = self::computeSignature($req, $apiSecret);
        $resp = self::postJson($endpoint, $req);

        // 按文档：响应签名需使用同方法校验（字段名 signature）
        if (array_key_exists(self::SIGNATURE_FIELD, $resp)) {
            $sig = is_string($resp[self::SIGNATURE_FIELD]) ? $resp[self::SIGNATURE_FIELD] : '';
            if ($sig !== '') {
                $respForSign = $resp;
                unset($respForSign[self::SIGNATURE_FIELD]);
                $expected = self::computeSignature($respForSign, $apiSecret);
                if (!hash_equals($expected, $sig)) {
                    throw new RuntimeException('DDPay response signature mismatch');
                }
            }
        }

        $statusCode = self::intValue($resp['status_code'] ?? 0);
        if ($statusCode !== 0) {
            $msg = is_string($resp['status_message'] ?? null) ? $resp['status_message'] : 'DDPay deposit initiation failed';
            throw new RuntimeException($msg);
        }

        return $resp;
    }

    /**
     * 出金发起（Payout Initiation）
     *
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public static function payoutInitiation(array $params): array
    {
        $endpoint = self::requireConfig('ddpay_payout_init_url');
        $apiSecret = self::requireConfig('ddpay_api_secret');

        $req = $params;
        if (isset($req[self::SIGNATURE_FIELD])) {
            unset($req[self::SIGNATURE_FIELD]);
        }

        $req[self::SIGNATURE_FIELD] = self::computeSignature($req, $apiSecret);
        $resp = self::postJson($endpoint, $req);

        // 按文档：响应签名需使用同方法校验（字段名 signature）
        if (array_key_exists(self::SIGNATURE_FIELD, $resp)) {
            $sig = is_string($resp[self::SIGNATURE_FIELD]) ? $resp[self::SIGNATURE_FIELD] : '';
            if ($sig !== '') {
                $respForSign = $resp;
                unset($respForSign[self::SIGNATURE_FIELD]);
                $expected = self::computeSignature($respForSign, $apiSecret);
                if (!hash_equals($expected, $sig)) {
                    throw new RuntimeException('DDPay response signature mismatch');
                }
            }
        }

        $statusCode = self::intValue($resp['status_code'] ?? 0);
        if ($statusCode !== 0) {
            $msg = is_string($resp['status_message'] ?? null) ? $resp['status_message'] : 'DDPay payout initiation failed';
            throw new RuntimeException($msg);
        }

        return $resp;
    }

    /**
     * 出金状态查询（Payout Status Inquiry）
     *
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public static function payoutStatusInquiry(array $params): array
    {
        $endpoint = self::requireConfig('ddpay_payout_status_url');
        $apiSecret = self::requireConfig('ddpay_api_secret');

        $req = $params;
        if (isset($req[self::SIGNATURE_FIELD])) {
            unset($req[self::SIGNATURE_FIELD]);
        }
        $req[self::SIGNATURE_FIELD] = self::computeSignature($req, $apiSecret);
        $resp = self::postJson($endpoint, $req);

        if (array_key_exists(self::SIGNATURE_FIELD, $resp)) {
            $sig = is_string($resp[self::SIGNATURE_FIELD]) ? $resp[self::SIGNATURE_FIELD] : '';
            if ($sig !== '') {
                $respForSign = $resp;
                unset($respForSign[self::SIGNATURE_FIELD]);
                $expected = self::computeSignature($respForSign, $apiSecret);
                if (!hash_equals($expected, $sig)) {
                    throw new RuntimeException('DDPay response signature mismatch');
                }
            }
        }

        $statusCode = self::intValue($resp['status_code'] ?? 0);
        if ($statusCode !== 0) {
            $msg = is_string($resp['status_message'] ?? null) ? $resp['status_message'] : 'DDPay payout status inquiry failed';
            throw new RuntimeException($msg);
        }

        return $resp;
    }

    /**
     * 校验 DDPay Webhook 通知签名。
     *
     * @param array<string, mixed> $payload
     */
    public static function verifyWebhookSignature(array $payload): bool
    {
        $apiSecret = self::requireConfig('ddpay_api_secret');
        $sigRaw = $payload[self::SIGNATURE_FIELD] ?? '';
        $sig = is_string($sigRaw) ? trim($sigRaw) : '';
        if ($sig === '') {
            return false;
        }

        $payloadForSign = $payload;
        unset($payloadForSign[self::SIGNATURE_FIELD]);
        $expected = self::computeSignature($payloadForSign, $apiSecret);

        return hash_equals($expected, $sig);
    }

    /**
     * @param array<string, mixed> $params
     */
    private static function computeSignature(array $params, string $apiSecret): string
    {
        // 1) 排除 signature & 空值/null
        $filtered = [];
        foreach ($params as $k => $v) {
            if (!is_string($k) || $k === '') {
                continue;
            }
            if ($k === self::SIGNATURE_FIELD) {
                continue;
            }
            if ($v === null) {
                continue;
            }
            if (is_string($v) && trim($v) === '') {
                continue;
            }
            if (is_bool($v)) {
                $filtered[$k] = $v ? 'true' : 'false';
                continue;
            }
            if (is_int($v) || is_float($v) || is_numeric($v)) {
                $filtered[$k] = strval($v);
                continue;
            }
            if (is_string($v)) {
                $filtered[$k] = trim($v);
                continue;
            }

            // 数组/对象等不应参与签名；忽略它们
            if (is_array($v) || is_object($v)) {
                continue;
            }

            $filtered[$k] = strval($v);
        }

        // 2) 按 ASCII 升序排序 key
        ksort($filtered, SORT_STRING);

        // 3) 拼接 key=value，用 & 连接
        $pairs = [];
        foreach ($filtered as $k => $v) {
            $pairs[] = $k . '=' . $v;
        }

        // 4) 追加 &key=API_SECRET
        $base = implode('&', $pairs);
        $signStr = $base . '&' . self::SECRET_KEY_PARAM . '=' . $apiSecret;

        // 5) MD5 小写
        $hash = md5($signStr);
        return is_string($hash) ? strtolower($hash) : '';
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    private static function postJson(string $url, array $payload): array
    {
        if (!function_exists('curl_init')) {
            throw new RuntimeException('curl extension is required for DDPayGateway');
        }

        $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($body) || $body === '') {
            throw new RuntimeException('DDPay request body encode failed');
        }

        $ch = curl_init($url);
        if ($ch === false) {
            throw new RuntimeException('DDPay curl_init failed');
        }

        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json; charset=utf-8',
            'Accept: application/json',
        ]);

        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        $respBody = curl_exec($ch);
        $errno = curl_errno($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        if ($respBody === false) {
            throw new RuntimeException('DDPay request failed: ' . ($errno > 0 ? 'curl_' . strval($errno) : 'unknown'));
        }
        if (!is_numeric($httpCode)) {
            throw new RuntimeException('DDPay request http code invalid');
        }
        $httpCodeInt = intval(strval($httpCode));
        if ($httpCodeInt < 200 || $httpCodeInt >= 300) {
            $snippet = '';
            if (is_string($respBody)) {
                $snippet = trim($respBody);
                if (mb_strlen($snippet) > 400) {
                    $snippet = mb_substr($snippet, 0, 400) . '...';
                }
            }
            $suffix = $snippet !== '' ? (' body=' . $snippet) : '';
            throw new RuntimeException('DDPay request http failed, http_code=' . strval($httpCodeInt) . $suffix);
        }

        $decoded = json_decode(is_string($respBody) ? $respBody : '', true);
        if (!is_array($decoded)) {
            throw new RuntimeException('DDPay response decode failed');
        }

        return $decoded;
    }

    /**
     * @return mixed
     */
    private static function requireConfig(string $key): mixed
    {
        $v = config('app.' . $key, '');
        if (!is_string($v)) {
            return '';
        }
        $s = trim($v);
        if ($s === '') {
            throw new InvalidArgumentException('Missing config: app.' . $key);
        }
        return $s;
    }

    /**
     * @param mixed $v
     */
    private static function intValue(mixed $v): int
    {
        if (is_int($v)) {
            return $v;
        }
        if (is_string($v) && $v !== '') {
            $n = filter_var($v, FILTER_VALIDATE_INT);
            return $n === false ? 0 : intval(strval($n));
        }
        if (is_numeric($v)) {
            $n = filter_var($v, FILTER_VALIDATE_INT);
            return $n === false ? 0 : intval(strval($n));
        }
        return 0;
    }
}

