<?php


namespace Fliegerlogin\VereinsfliegerApi\Api;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Der Sniff haelt jede Interpolation fuer Ausgabe. Hier entsteht keine: die Werte gehen in Exception-Objekte, die intern gefangen und erst nach dem Maskieren angezeigt werden.


use Fliegerlogin\Guard;
use Fliegerlogin\Debug;
use Fliegerlogin\RequestCounter;
use Fliegerlogin\VereinsfliegerApi\Exceptions\HttpException;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerException;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

abstract class AbstractApi {

	/** @var string  */
	protected $baseUrl = 'https://vereinsflieger.de/interface/rest';

	/** @var string|null  */
	public $accesstoken = null;

	/** @var ClientInterface */
	protected $client;

	/** @var ObjectNormalizer */
	protected $normalizer;

	/** @var Serializer */
	protected $serializer;

	public function __construct(?ClientInterface $client = null) {
		$this->client = $client ?? self::createDefaultClient();

		$typeExtractor = new PropertyInfoExtractor(
			[new ReflectionExtractor()],
			[new PhpDocExtractor(), new ReflectionExtractor()]
		);

		$this->normalizer = new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter(), null, $typeExtractor);
		$this->serializer = new Serializer(
			[new ArrayDenormalizer(), new ObjectNormalizer(null, null, null, $typeExtractor)],
			[new JsonEncoder()]
		);
	}

	public function setBaseUrl(string $baseUrl): void {
		$this->baseUrl = $baseUrl;
	}

	protected function getHttpOptions(array $mergeWith = [], bool $disableAccessToken = false) {
		return array_merge_recursive(
			[
				'http_errors' => false
			],
			$this->accesstoken && !$disableAccessToken ? [
				'query' => ['accesstoken' => $this->accesstoken]
			] : [],
			$mergeWith);
	}

	protected function serializeRequestParams($params): array {
		return json_decode($this->serializer->serialize($this->normalizer->normalize($params), 'json'), JSON_OBJECT_AS_ARRAY);
	}

	protected function deserializeResponse(ResponseInterface $response, string $targetType) {
		try {
			return $this->serializer->deserialize($response->getBody()->getContents(), $targetType, 'json', [
				AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true,
			]);
		} catch (SerializerException $e) {
			throw new HttpException($response);
		}
	}

	private static function createDefaultClient(): Client {
		$stack = HandlerStack::create();
		$stack->push(self::counterMiddleware(), 'fliegerlogin_request_counter');
		if (Debug::isEnabled()) {
			$stack->push(self::debugMiddleware(), 'fliegerlogin_debug');
		}

		return new Client(['handler' => $stack]);
	}

	private static function counterMiddleware(): callable {
		return function (callable $handler): callable {
			return function (RequestInterface $request, array $options) use ($handler) {
				// Feed the global circuit breaker's sliding window with every
				// outbound request, even ones that later error out.
				Guard::recordApiRequest();
				return $handler($request, $options)->then(
					function (ResponseInterface $response): ResponseInterface {
						RequestCounter::increment();
						return $response;
					}
				);
			};
		};
	}

	private static function debugMiddleware(): callable {
		return function (callable $handler): callable {
			return function (RequestInterface $request, array $options) use ($handler) {
				Debug::log('http.request', [
					'method' => $request->getMethod(),
					'url'    => (string) $request->getUri(),
				]);
				return $handler($request, $options)->then(
					function (ResponseInterface $response) use ($request): ResponseInterface {
						$body = (string) $response->getBody();
						$response->getBody()->rewind();
						$headers = [];
						foreach ($response->getHeaders() as $name => $values) {
							$headers[$name] = implode(', ', $values);
						}
						Debug::log('http.response', [
							'method'  => $request->getMethod(),
							'url'     => (string) $request->getUri(),
							'status'  => $response->getStatusCode(),
							'headers' => $headers,
							'body'    => substr($body, 0, 500),
						]);
						return $response;
					}
				);
			};
		};
	}

}