<?php

namespace Opentalent\OtCore\Domain\Repository;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Opentalent\OtCore\Exception\ApiRequestException;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Core\ApplicationContext;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;

/**
 *  Base class for repositories based on the Opentalent API
 *
 */
abstract class BaseApiRepository implements LoggerAwareInterface
{
    use LoggerAwareTrait;

    const DEFAULT_BASE_URI = 'https://api.opentalent.fr/api/';
    const URI_TRAILING_PART = '';
    const HYDRA_TYPE = '';
    const HTTP_METHOD = 'GET';
    const DEFAULT_ITEMS_PER_PAGE = 8;

    protected string $base_uri = self::DEFAULT_BASE_URI;
    protected array $variants_uris = [];
    protected Client $client;
    protected ApplicationContext $context;

    /**
     * BaseApiRepository constructor.
     *
     * @param ObjectManagerInterface $objectManager
     * @param Client|null $client  [For tests only]
     * @param ApplicationContext|null $context  [For tests only]
     */
    public function __construct(
        ?Client $client = null,
        ?ApplicationContext $context = null
    ) {
        if ($context === null) {
            $this->context = \TYPO3\CMS\Core\Core\Environment::getContext();
        } else {
            $this->context = $context;
        }

        $this->loadConf();

        if ($client === null) {
            $this->client = new Client(['base_uri' => $this->getApiUri()]);
        } else {
            $this->client = $client;
        }
    }

    private function loadConf() {
        $conf_path = $_ENV['TYPO3_PATH_ROOT'] . '/typo3conf/ext/ot_core/Configuration/ot_config.yaml';
        $conf = Yaml::parseFile($conf_path);

        // api_variant_uri: Should we set an alternative uri for the API? (dev and testing only)
        if ($this->context->isDevelopment() || $this->context->isTesting()) {
            $this->variants_uris = $conf['api_variant_uri'];
        }
    }

    /**
     * Return the API URI for the current repository
     *
     * @param string $trailing_part
     * @return string
     */
    protected function getApiUri(string $trailing_part = null): string
    {
        $host = $_SERVER['HTTP_HOST'];
        if (isset($this->variants_uris[$host])) {
            $uri = $this->variants_uris[$host];
        } else {
            $uri = self::DEFAULT_BASE_URI;
        }

        $trailing_part = $trailing_part ?? $this::URI_TRAILING_PART;
        $uri = trim($uri, '/') . '/' . trim($trailing_part, '/');
        return $uri;
    }

    /**
     * [FOR TESTS ONLY]
     * @param Client $client
     */
    protected function injectClient(Client $client) {
        $this->client = $client;
    }

    /**
     * Send a request to the API and
     * returns the records as an array (members)
     *
     * @param array $params
     * @param string|null $forceUri
     * @return ApiPagedCollection
     * @throws ApiRequestException
     */
    protected function getApiRecords(array $params = [], ?string $forceUri = null): ApiPagedCollection
    {
        $uri = $forceUri ?? $this->getApiUri();

        $body = $this->getJsonDecoded($uri, $params);

        $page = (int)($params['page'] ?? 1);

        // build up the members
        $members = [];

        if (is_array($body['hydra:member'])) {
            foreach ($body['hydra:member'] as $record) {
                $instance = $this->memberToObject($record);
                if ($instance != null) {
                    $members[] = $instance;
                }
            }
        }

        return new ApiPagedCollection(
            (int)($body['hydra:totalItems'] ?? 0),
            (int)($body['hydra:itemsPerPage'] ?? 0),
            $page,
            $members
        );
    }

    /**
     * -- Needs to be reimplemented in subclasses --
     * Convert response's members record to an actual Domain's object
     * @param array $record
     * @return object
     */
    abstract protected function memberToObject(array $record);

    /**
     * Send a request to the API and
     * returns the first record (member)
     *
     * @param array $params
     * @param string|null $forceUri
     * @return object
     * @throws ApiRequestException
     */
    protected function getApiFirstRecord($params = [], ?string $forceUri = null)
    {
        $params['page'] = '1';
        $params['totalItems'] = '1';
        $collection = $this->getApiRecords($params, $forceUri);
        return $collection->getMembers()[0];
    }

    /**
     * Send a request to the API and
     * returns the Json response as an array
     *
     * @param string $uri
     * @param array $params
     * @return array
     * @throws ApiRequestException
     */
    protected function getJsonDecoded(string $uri, $params = []): array
    {
        return json_decode($this->getBody($uri, $params),true);
    }

    /**
     * Send a request to the API and returns
     * the response's body as a string
     *
     * @param string $uri
     * @param array $params
     * @return string
     * @throws ApiRequestException
     */
    protected function getBody(string $uri, $params = [])
    {
        return (string)$this->getResponse($uri, $params)->getBody();
    }

    /**
     * Send a request to the API and returns
     * the result as a Response object
     *
     * @param string $uri
     * @param array $params
     * @return ResponseInterface
     * @throws ApiRequestException
     */
    protected function getResponse(string $uri, $params = []): ResponseInterface
    {
        $uri = $uri . '?_format=json';
        if(!isset($params['itemsPerPage'])) {
            $params['itemsPerPage'] = (string)self::DEFAULT_ITEMS_PER_PAGE;
        }
        if (!empty($params)) {
            $uri = $uri . '&' . http_build_query($params);
        }
        try {
            if ($this->context->isDevelopment()) {
                $this->logger->info('API Call: ' . $uri);
            }
            return $this->client->request(static::HTTP_METHOD, $uri);
        } catch (GuzzleException $e) {
            throw ApiRequestException::from_exception($e);
        }
    }
}
