<?php declare(strict_types=1);

namespace WeChatPay;

use function strlen;
use function sprintf;
use function in_array;
use function array_key_exists;

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Query;
use GuzzleHttp\Psr7\Utils;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\MessageInterface;

/**
 * XML based Client interface for sending HTTP requests.
 */
trait ClientXmlTrait
{
    /**
     * @var array<string, string> - The default headers whose passed in `GuzzleHttp\Client`.
     */
    protected static $headers = [
        'Accept' => 'text/xml, text/plain, application/x-gzip',
        'Content-Type' => 'text/xml; charset=utf-8',
    ];

    /**
     * @var string[] - Special URLs whose were designed that none signature respond.
     */
    protected static $noneSignatureRespond = [
        '/mchrisk/querymchrisk',
        '/mchrisk/setmchriskcallback',
        '/mchrisk/syncmchriskresult',
        '/mmpaymkttransfers/gethbinfo',
        '/mmpaymkttransfers/gettransferinfo',
        '/mmpaymkttransfers/pay_bank',
        '/mmpaymkttransfers/promotion/paywwsptrans2pocket',
        '/mmpaymkttransfers/promotion/querywwsptrans2pocket',
        '/mmpaymkttransfers/promotion/transfers',
        '/mmpaymkttransfers/query_bank',
        '/mmpaymkttransfers/sendgroupredpack',
        '/mmpaymkttransfers/sendminiprogramhb',
        '/mmpaymkttransfers/sendredpack',
        '/papay/entrustweb',
        '/papay/h5entrustweb',
        '/papay/partner/entrustweb',
        '/papay/partner/h5entrustweb',
        '/pay/downloadbill',
        '/pay/downloadfundflow',
        '/payitil/report',
        '/risk/getpublickey',
        '/risk/getviolation',
        '/secapi/mch/submchmanage',
        '/xdc/apiv2getsignkey/sign/getsignkey',
    ];

    abstract protected static function body(MessageInterface $message): string;

    abstract protected static function withDefaults(array ...$config): array;

    /**
     * APIv2's transformRequest, did the `datasign` and `array2xml` together
     *
     * @param ?string $mchid - The merchant ID
     * @param string $secret - The secret key string (optional)
     * @param array{cert?: ?string, key?: ?string} $merchant - The merchant private key and certificate array. (optional)
     *
     * @return callable(callable(RequestInterface, array))
     * @throws \WeChatPay\Exception\InvalidArgumentException
     */
    public static function transformRequest(
        ?string $mchid = null,
        #[\SensitiveParameter]
        string $secret = '',
        ?array $merchant = null
    ): callable
    {
        return static function (callable $handler) use ($mchid, $secret, $merchant): callable {
            return static function (RequestInterface $request, array $options = []) use ($handler, $mchid, $secret, $merchant): PromiseInterface {
                $methodIsGet = $request->getMethod() === 'GET';

                if ($methodIsGet) {
                    $queryParams = Query::parse($request->getUri()->getQuery());
                }

                $data = $options['xml'] ?? ($queryParams ?? []);

                if ($mchid && $mchid !== ($inputMchId = $data['mch_id'] ?? $data['mchid'] ?? $data['combine_mch_id'] ?? null)) {
                    throw new Exception\InvalidArgumentException(sprintf(Exception\EV2_REQ_XML_NOTMATCHED_MCHID, $inputMchId ?? '', $mchid));
                }

                $type = $data['sign_type'] ?? Crypto\Hash::ALGO_MD5;

                isset($options['nonceless']) || $data['nonce_str'] = $data['nonce_str'] ?? Formatter::nonce();

                $data['sign'] = Crypto\Hash::sign($type, Formatter::queryStringLike(Formatter::ksort($data)), $secret);

                $modify = $methodIsGet ? ['query' => Query::build($data)] : ['body' => Transformer::toXml($data)];

                // for security request, it was required the merchant's private_key and certificate
                if (isset($options['security']) && true === $options['security']) {
                    $options['ssl_key'] = $merchant['key'] ?? null;
                    $options['cert'] = $merchant['cert'] ?? null;
                }

                unset($options['xml'], $options['nonceless'], $options['security']);

                return $handler(Utils::modifyRequest($request, $modify), $options);
            };
        };
    }

    /**
     * APIv2's transformResponse, doing the `xml2array` then `verify` the signature job only
     *
     * @param string $secret - The secret key string (optional)
     *
     * @return callable(callable(RequestInterface, array))
     */
    public static function transformResponse(
        #[\SensitiveParameter]
        string $secret = ''
    ): callable
    {
        return static function (callable $handler) use ($secret): callable {
            return static function (RequestInterface $request, array $options = []) use ($secret, $handler): PromiseInterface {
                if (in_array($request->getUri()->getPath(), static::$noneSignatureRespond)) {
                    return $handler($request, $options);
                }

                return $handler($request, $options)->then(static function(ResponseInterface $response) use ($secret) {
                    $result = Transformer::toArray(static::body($response));

                    if (!(array_key_exists('return_code', $result) && Crypto\Hash::equals('SUCCESS', $result['return_code']))) {
                        return Create::rejectionFor($response);
                    }

                    if (array_key_exists('result_code', $result) && !Crypto\Hash::equals('SUCCESS', $result['result_code'])) {
                        return Create::rejectionFor($response);
                    }

                    /** @var ?string $sign */
                    $sign = $result['sign'] ?? null;
                    $type = $sign && strlen($sign) === 64 ? Crypto\Hash::ALGO_HMAC_SHA256 : Crypto\Hash::ALGO_MD5;
                    /** @var string $calc - calculated digest string, it's naver `null` here because of \$type known. */
                    $calc = Crypto\Hash::sign($type, Formatter::queryStringLike(Formatter::ksort($result)), $secret);

                    return Crypto\Hash::equals($calc, $sign) ? $response : Create::rejectionFor($response);
                });
            };
        };
    }

    /**
     * Create an APIv2's client
     *
     * @deprecated 1.0 - @see \WeChatPay\Exception\WeChatPayException::DEP_XML_PROTOCOL_IS_REACHABLE_EOL
     *
     * Optional acceptable \$config parameters
     *   - mchid?: ?string - The merchant ID
     *   - secret?: ?string - The secret key string
     *   - merchant?: array{key?: string, cert?: string} - The merchant private key and certificate array. (optional)
     *   - merchant<?key, string|string[]> - The merchant private key(file path string). (optional)
     *   - merchant<?cert, string|string[]> - The merchant certificate(file path string). (optional)
     *
     * @param array<string,string|int|bool|array|mixed> $config - The configuration
     */
    public static function xmlBased(array $config = []): Client
    {
        /** @var HandlerStack $stack */
        $stack = isset($config['handler']) && ($config['handler'] instanceof HandlerStack) ? (clone $config['handler']) : HandlerStack::create();
        $stack->before('prepare_body', static::transformRequest($config['mchid'] ?? null, $config['secret'] ?? '', $config['merchant'] ?? []), 'transform_request');
        $stack->before('http_errors', static::transformResponse($config['secret'] ?? ''), 'transform_response');
        $config['handler'] = $stack;

        unset($config['mchid'], $config['serial'], $config['privateKey'], $config['certs'], $config['secret'], $config['merchant']);

        return new Client(static::withDefaults(['headers' => static::$headers], $config));
    }
}
