<?php

declare (strict_types=1);
namespace CelerSearch\Vendor\Meilisearch\Http;

use CelerSearch\Vendor\Http\Discovery\Psr17FactoryDiscovery;
use CelerSearch\Vendor\Http\Discovery\Psr18ClientDiscovery;
use CelerSearch\Vendor\Meilisearch\Contracts\Http;
use CelerSearch\Vendor\Meilisearch\Exceptions\ApiException;
use CelerSearch\Vendor\Meilisearch\Exceptions\CommunicationException;
use CelerSearch\Vendor\Meilisearch\Exceptions\InvalidResponseBodyException;
use CelerSearch\Vendor\Meilisearch\Http\Serialize\Json;
use CelerSearch\Vendor\Meilisearch\Meilisearch;
use CelerSearch\Vendor\Psr\Http\Client\ClientExceptionInterface;
use CelerSearch\Vendor\Psr\Http\Client\ClientInterface;
use CelerSearch\Vendor\Psr\Http\Client\NetworkExceptionInterface;
use CelerSearch\Vendor\Psr\Http\Message\RequestFactoryInterface;
use CelerSearch\Vendor\Psr\Http\Message\RequestInterface;
use CelerSearch\Vendor\Psr\Http\Message\ResponseInterface;
use CelerSearch\Vendor\Psr\Http\Message\StreamFactoryInterface;
use CelerSearch\Vendor\Psr\Http\Message\StreamInterface;
/** @internal */
class Client implements Http
{
    private ClientInterface $http;
    private RequestFactoryInterface $requestFactory;
    private StreamFactoryInterface $streamFactory;
    /**
     * @var array<string, string|string[]>
     */
    private array $headers;
    /**
     * @var non-empty-string
     */
    private string $baseUrl;
    private Json $json;
    /**
     * @param non-empty-string   $url
     * @param array<int, string> $clientAgents
     */
    public function __construct(string $url, ?string $apiKey = null, ?ClientInterface $httpClient = null, ?RequestFactoryInterface $reqFactory = null, array $clientAgents = [], ?StreamFactoryInterface $streamFactory = null)
    {
        $this->baseUrl = $url;
        $this->http = $httpClient ?? Psr18ClientDiscovery::find();
        $this->requestFactory = $reqFactory ?? Psr17FactoryDiscovery::findRequestFactory();
        $this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
        $this->headers = ['User-Agent' => \implode(';', \array_merge($clientAgents, [Meilisearch::qualifiedVersion()]))];
        if (null !== $apiKey && '' !== $apiKey) {
            $this->headers['Authorization'] = \sprintf('Bearer %s', $apiKey);
        }
        $this->json = new Json();
    }
    /**
     * @throws ClientExceptionInterface
     * @throws ApiException
     * @throws CommunicationException
     */
    public function get(string $path, array $query = []) : mixed
    {
        $request = $this->requestFactory->createRequest('GET', $this->baseUrl . $path . $this->buildQueryString($query));
        return $this->execute($request);
    }
    /**
     * @param non-empty-string|null $contentType
     *
     * @throws ApiException
     * @throws ClientExceptionInterface
     * @throws CommunicationException
     * @throws \JsonException
     */
    public function post(string $path, mixed $body = null, array $query = [], ?string $contentType = null) : mixed
    {
        if (null === $contentType) {
            $body = $this->json->serialize($body);
        }
        $request = $this->requestFactory->createRequest('POST', $this->baseUrl . $path . $this->buildQueryString($query))->withBody($this->streamFactory->createStream($body));
        return $this->execute($request, ['Content-type' => $contentType ?? 'application/json']);
    }
    /**
     * @param non-empty-string|null $contentType
     *
     * @throws ApiException
     * @throws ClientExceptionInterface
     * @throws CommunicationException
     * @throws \JsonException
     */
    public function put(string $path, mixed $body = null, array $query = [], ?string $contentType = null) : mixed
    {
        if (null === $contentType) {
            $body = $this->json->serialize($body);
        }
        $request = $this->requestFactory->createRequest('PUT', $this->baseUrl . $path . $this->buildQueryString($query))->withBody($this->streamFactory->createStream($body));
        return $this->execute($request, ['Content-type' => $contentType ?? 'application/json']);
    }
    public function patch(string $path, mixed $body = null, array $query = []) : mixed
    {
        $request = $this->requestFactory->createRequest('PATCH', $this->baseUrl . $path . $this->buildQueryString($query))->withBody($this->streamFactory->createStream($this->json->serialize($body)));
        return $this->execute($request, ['Content-type' => 'application/json']);
    }
    public function delete(string $path, array $query = []) : mixed
    {
        $request = $this->requestFactory->createRequest('DELETE', $this->baseUrl . $path . $this->buildQueryString($query));
        return $this->execute($request);
    }
    /**
     * @throws ApiException
     * @throws ClientExceptionInterface
     * @throws CommunicationException
     * @throws \JsonException
     */
    public function postStream(string $path, mixed $body = null, array $query = []) : StreamInterface
    {
        $request = $this->requestFactory->createRequest('POST', $this->baseUrl . $path . $this->buildQueryString($query))->withBody($this->streamFactory->createStream($this->json->serialize($body)));
        return $this->executeStream($request, ['Content-type' => 'application/json']);
    }
    /**
     * @param array<string, string|string[]> $headers
     *
     * @throws ApiException
     * @throws ClientExceptionInterface
     * @throws CommunicationException
     */
    private function execute(RequestInterface $request, array $headers = [])
    {
        foreach (\array_merge($this->headers, $headers) as $header => $value) {
            $request = $request->withAddedHeader($header, $value);
        }
        try {
            return $this->parseResponse($this->http->sendRequest($request));
        } catch (NetworkExceptionInterface $e) {
            throw new CommunicationException($e->getMessage(), $e->getCode(), $e);
        }
    }
    /**
     * @param array<string, string|string[]> $headers
     *
     * @throws ApiException
     * @throws ClientExceptionInterface
     * @throws CommunicationException
     */
    private function executeStream(RequestInterface $request, array $headers = []) : StreamInterface
    {
        foreach (\array_merge($this->headers, $headers) as $header => $value) {
            $request = $request->withAddedHeader($header, $value);
        }
        try {
            $response = $this->http->sendRequest($request);
            if ($response->getStatusCode() >= 300) {
                $bodyContent = (string) $response->getBody();
                // Try to parse as JSON for structured errors, fall back to raw content
                if ($this->isJSONResponse($response->getHeader('content-type'))) {
                    try {
                        $body = $this->json->unserialize($bodyContent) ?? $response->getReasonPhrase();
                    } catch (\JsonException $e) {
                        $body = '' !== $bodyContent ? $bodyContent : $response->getReasonPhrase();
                    }
                } else {
                    $body = '' !== $bodyContent ? $bodyContent : $response->getReasonPhrase();
                }
                throw new ApiException($response, $body);
            }
            return $response->getBody();
        } catch (NetworkExceptionInterface $e) {
            throw new CommunicationException($e->getMessage(), $e->getCode(), $e);
        }
    }
    private function buildQueryString(array $queryParams = []) : string
    {
        foreach ($queryParams as $key => $value) {
            if (\is_bool($value)) {
                $queryParams[$key] = $value ? 'true' : 'false';
            }
            if (\is_array($value) && \array_is_list($value)) {
                $queryParams[$key] = \implode(',', $value);
            }
        }
        return [] !== $queryParams ? '?' . \http_build_query($queryParams) : '';
    }
    /**
     * @throws ApiException
     * @throws InvalidResponseBodyException
     * @throws \JsonException
     */
    private function parseResponse(ResponseInterface $response)
    {
        if (204 === $response->getStatusCode()) {
            return null;
        }
        if (!$this->isJSONResponse($response->getHeader('content-type'))) {
            throw new InvalidResponseBodyException($response, (string) $response->getBody());
        }
        if ($response->getStatusCode() >= 300) {
            $body = $this->json->unserialize((string) $response->getBody()) ?? $response->getReasonPhrase();
            throw new ApiException($response, $body);
        }
        return $this->json->unserialize((string) $response->getBody());
    }
    /**
     * Checks if any of the header values indicate a JSON response.
     *
     * @param array $headerValues the array of header values to check
     *
     * @return bool true if any header value contains 'application/json', otherwise false
     */
    private function isJSONResponse(array $headerValues) : bool
    {
        return [] !== \array_filter($headerValues, static fn(string $v) => \str_contains($v, 'application/json'));
    }
}
