<?php
namespace App\Utils\HttpTransfer;

/**
 * Wrapper on Guzzle that fills a log context with the request and response.
 * NOTE: This is a rough equivalent to the native file_get_contents()
 *  - it is not designed to be used with async operations or streaming responses
 *  - the response body not streamed, it is fully read and stored in memory!
 *  - this class is intended for simple HTTP requests where you need detailed logging of the transfer
 *  - it is not suitable for large file downloads or long-running requests
 */
class GuzzleRequestWithLogging
{
	/**
	 * @var \GuzzleHttp\Psr7\Request
	 */
	private $request = null;

	/**
	 * @var \GuzzleHttp\Psr7\Response
	 */
	private $response = null;

	/**
	 * @var string|null
	 */
	private $responseBody = null;

	/**
	 * Log context built via middleware (request, response, timings)
	 * @var array
	 */
	private $logContext = [];

	/**
	 * Get the last request
	 * @return \GuzzleHttp\Psr7\Request
	 */
	public function getLastRequest(): \GuzzleHttp\Psr7\Request
	{
		return $this->request;
	}

	/**
	 * Get the last response
	 * @return \GuzzleHttp\Psr7\Response
	 */
	public function getLastResponse(): \GuzzleHttp\Psr7\Response
	{
		return $this->response;
	}

	/**
	 * Get the last response body
	 * @return string
	 */
	public function getLastResponseBody(): string|null
	{
		return $this->responseBody;
	}

	/**
	 * Return the log context captured by the middleware.
	 * NOTE: This is intended to be passed directly to the app logger with minimal changes
	 *
	 * @return array
	 */
	public function getLogContext(): array
	{
		return $this->logContext;
	}

	/**
	 * Build the internal Guzzle client with middleware that captures request/response and timings into $this->logContext.
	 * NOTE: This is supposed to be used only in sync mode, as the middleware is manually calling wait at some point and I'm not sure if that interferes with sync/async flows.
	 * NOTE: The response body is fully read and stored in memory.
	 * @return \GuzzleHttp\Client
	 */
	protected function buildGuzzleSyncClient(): \GuzzleHttp\Client
	{
		$handlerStack = \GuzzleHttp\HandlerStack::create();

		// Use tap middleware to observe request and response and record timing
		$handlerStack->push(
			\GuzzleHttp\Middleware::tap(
				function ($request, array $options) {
					// mark the request start time for duration calculation
					$this->logContext = [
						'request' => new \Engine\Logger\Context\GuzzleTransfer($request, (string)$request->getBody()),
						'response' => null,
						'duration' => null,
						'_startTime' => microtime(true),
					];
				},
				function ($request, array $options, $response) {
					// calculate duration
					$duration = microtime(true) - $this->logContext['_startTime'];
					unset($this->logContext['_startTime']);

					$exception = null;

					// unwrap promises if needed
					if ($response instanceof \GuzzleHttp\Promise\PromiseInterface) {
						try {
							// NOTE: this call might be breaking the async flows
							$response = $response->wait(); // returns ResponseInterface or throws
						}
						catch (\GuzzleHttp\Exception\GuzzleException $ex) {
							$exception = $ex;
							$response = null;
						}
					}

					$responseObj = null;
					$responseStr = null;
					if ($response) {
						// $response may be a ResponseInterface or an exception reason
						if ($response instanceof \Psr\Http\Message\ResponseInterface) {
							$responseObj = $response;
							$responseStr = (string)$response->getBody();				// NOTE: body is fully read into memory
						}
						elseif ($response instanceof \GuzzleHttp\Exception\RequestException && $response->getResponse()) {
							$responseObj = $response->getResponse();
							$responseStr = (string)$response->getResponse()->getBody();	// NOTE: body is fully read into memory
						}

						$this->logContext['response'] =  new \Engine\Logger\Context\GuzzleTransfer($responseObj, $responseStr);
					}

					if ($exception) {
						$this->logContext['exception'] = new \Engine\Logger\Context\ExceptionAsHtml($exception);
					}

					$this->logContext['duration'] =  $duration;
					$this->response = $responseObj;
					$this->responseBody = $responseStr;
				}
			)
		);

		return new \GuzzleHttp\Client([
			'handler' => $handlerStack,
		]);
	}

	/**
	 * Execute the request
	 * @param string $method (GET|POST|PUT)
	 * @param string $url
	 * @param array $headers
	 * @param string $body
	 * @param array $options
	 * @return string|null
	 */
	public function send($method, $url, $headers=[], $body=null, $options=[]): string|null
	{
		// setup a default User-Agent
		if (!isset($headers['User-Agent'])) {
			$headers['User-Agent'] = 'GuzzleRequestWithLogging';
		}

		if (!isset($options['timeout'])) {
			$options['timeout'] = 10;
		}
		if (!isset($options['connect_timeout'])) {
			$options['connect_timeout'] = 5;
		}

		$this->request = new \GuzzleHttp\Psr7\Request($method, $url, $headers, $body);
		$this->response = $this->buildGuzzleSyncClient()->send($this->request, $options);
		return $this->responseBody;
	}

}
