<?php

namespace common\modules\orderPayment\WOP\OnlinePayments\Core\Infrastructure\Http;

use common\modules\orderPayment\WOP\OnlinePayments\Core\Infrastructure\Http\Exceptions\HttpRequestException;
use common\modules\orderPayment\WOP\OnlinePayments\Core\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException;
/**
 * Class AsyncSocketHttpClient
 *
 * AsyncSocketHttpClient will send asynchronous web request by using the
 * web sockets as supported by php.
 *
 * Main purpose of the AsyncSocketHttpClient is to improve the performance
 * of the async requests especially in a multi tenant environment where
 * the major task execution bottleneck is the task starting process, since on
 * average it takes about 1 second for the curl library to actually perform the
 * request, therefore, minimal time necessary to send async request is around
 * one second or more.
 *
 * @important notice: Socket client will use curl for synchronous request. Since
 * there are no performance benefits to be had, and supporting full synchronous
 * request would imply supporting complete HTTP specification.
 *
 * @package OnlinePayments\Core\Infrastructure\Http
 */
class AsyncSocketHttpClient extends CurlHttpClient
{
    /** @var int  */
    const DEFAULT_ASYNC_REQUEST_TIMEOUT = 5;
    /** @var int  */
    const FWRITE_SLEEP_USECONDS = 300000;
    /**
     * Create and send request asynchronously.
     *
     * @param string $method HTTP method (GET, POST, PUT, DELETE etc.)
     * @param string $url Request URL. Full URL where request should be sent.
     * @param array|null $headers Request headers to send. Key as header name and value as header content. Optional.
     * @param string $body Request payload. String data to send as HTTP request payload. Optional.  Default value for
     * request body is '1' to ensure minimal request data in case of POST, PUT, PATCH methods.
     *
     * @throws HttpRequestException
     */
    protected function sendHttpRequestAsync(string $method, string $url, ?array $headers = [], string $body = '1'): void
    {
        $url = $this->adjustUrlIfNeeded($url);
        $urlDetails = parse_url($url);
        if ($urlDetails === \false) {
            throw new HttpRequestException('Unable to parse request url.');
        }
        $transferProtocol = $this->getTransferProtocol($urlDetails);
        $port = $this->getTargetPort($urlDetails);
        $path = $this->getPath($urlDetails);
        $payload = $this->getRequestPayload(strtoupper($method), $urlDetails['host'], $path, $headers, $body);
        $timeOut = $this->getRequestTimeOut();
        $this->executeRequest($transferProtocol, $urlDetails['host'], $port, $timeOut, $payload);
    }
    /**
     * Deduces transfer protocol based ont the url scheme.
     *
     * @param array $urlDetails URL details formatted as the output of the parse_url method.
     *
     * @return string Returns ssl:// if scheme is HTTPS, tcp:// otherwise.
     */
    protected function getTransferProtocol(array $urlDetails): string
    {
        if ($urlDetails['scheme'] === 'https') {
            return 'tls://';
        }
        return 'tcp://';
    }
    /**
     * Provides request port based on the url details.
     *
     * If the port is defined in the URL returns defined port;
     * Otherwise, if the scheme is HTTPS returns 443;
     * Otherwise, returns 80.
     *
     * @param array $urlDetails URL details formatted as the output of the parse_url method.
     *
     * @return int Request port.
     */
    protected function getTargetPort(array $urlDetails): int
    {
        if (!empty($urlDetails['port'])) {
            return $urlDetails['port'];
        }
        if ($urlDetails['scheme'] === 'https') {
            return 443;
        }
        return 80;
    }
    /**
     * Retrieves request path based on url details.
     *
     * @param array $urlDetails URL details formatted as the output of the parse_url method.
     *
     * @return string Request path.
     */
    protected function getPath(array $urlDetails): string
    {
        return !empty($urlDetails['path']) ? $urlDetails['path'] : '/';
    }
    /**
     * Retrieves request time out in seconds.
     *
     * @return int Request timeout in seconds.
     *
     * @throws QueryFilterInvalidParamException
     */
    protected function getRequestTimeOut(): int
    {
        $timeout = $this->getConfigService()->getAsyncRequestTimeout();
        return !empty($timeout) ? $timeout : static::DEFAULT_ASYNC_REQUEST_TIMEOUT;
    }
    /**
     * Generates request payload in accordance with the HTTP 1.1.
     *
     * @param string $method Request HTTP method.
     * @param string $host Request host.
     * @param string $path Request path.
     * @param array $headers List of request headers.
     * @param string $body Request body.
     *
     * @return string
     */
    protected function getRequestPayload(string $method, string $host, string $path, array $headers, string $body): string
    {
        $payload = "{$method} {$path} HTTP/1.1\r\n";
        $payload .= "Host: {$host}\r\n";
        foreach ($headers as $header => $value) {
            $payload .= $header . (!empty($value) ? ": {$value}" : '') . "\r\n";
        }
        $payload .= "Content-Length: " . strlen($body) . "\r\n";
        $payload .= "Connection: close\r\n\r\n";
        $payload .= $body . "\r\n\r\n";
        return $payload;
    }
    /**
     * Executes request by writing to the php web socket.
     *
     * @param string $transferProtocol One of 'ssl://' or 'tcp://'.
     * @param string $host Request host.
     * @param int $port Destination port.
     * @param int $timeOut Request timeout in seconds.
     * @param string $payload Payload to be written to the socket.
     *
     * @throws HttpRequestException Thrown when the request
     *      is not completed successfully.
     */
    protected function executeRequest(string $transferProtocol, string $host, int $port, int $timeOut, string $payload)
    {
        $socket = pfsockopen($transferProtocol . $host, $port, $errorCode, $errorMsg, $timeOut);
        if ($socket === \false) {
            throw new HttpRequestException($errorMsg, $errorCode);
        }
        $writeResult = fwrite($socket, $payload);
        if ($writeResult === \false) {
            throw new HttpRequestException('Unable to write to php socket.');
        }
        // Even though php reports that the write has been completed
        // The data is not necessarily been written
        // We must wait to make sure that the write is actually complete
        usleep(self::FWRITE_SLEEP_USECONDS);
        fclose($socket);
    }
}
