<?php
namespace API\InseeSiret;

use Entity\Model\Dilispec;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Utils;
use API\Provigis\Exception\ProvigisWsResponseBodyException;
use API\Provigis\Exception\ProvigisWsResponseException;
use Helpers\Fichier\Fichier;
use Helpers\Log\LogSysteme;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use Helpers\Session\ParseBase1;
use Psr\Http\Message\ResponseInterface;

/**
 * @see https://api.insee.fr/catalogue/site/themes/wso2/subthemes/insee/pages/item-info.jag?name=Sirene&version=V3&provider=insee
 * @_phpstan-type SearchResultItem object{
 *     siren:string,
 *     siret:string,
 *     uniteLegale:object{
 *         denominationUniteLegale:string,
 *         dateCreationEtablissement:string,
 *     },
 *     adresseEtablissement:object{
 *         complementAdresseEtablissement:string,
 *         numeroVoieEtablissement:string,
 *         typeVoieEtablissement:string,
 *         libelleVoieEtablissement:string,
 *         codePostalEtablissement:string,
 *         libelleCommuneEtablissement:string,
 *         codeCommuneEtablissement:string
 *     },
 *     adresse2Etablissement:object{TODO:string},
 *     periodesEtablissement:array<object{
 *         dateFin:?string,
 *         dateDebut:?string,
 *         TODO:string
 *     }>
 * }[]
 *
 */
class SiretWsService
{
    private ?Client $client = null;
    private string $baseUrl; //= 'https://api.insee.fr/entreprises/sirene/V3.11';
    private string $baseUrlAuth; //= 'https://api.insee.fr';

    private string $login;
    private string $password;

//_____________________________________________________________________________
// Constructor

    private function __construct(
        string $baseUrl,
        string $baseUrlAuth,
        string $login,
        string $password
    ) {
        $this->baseUrl = trim($baseUrl, '/');
        $this->baseUrlAuth = trim($baseUrlAuth, '/');
        $this->login = $login;
        $this->password = $password;
    }

    public static function createFromConfig(ContainerInterface $container) :SiretWsService
    {
        /** @var \PDO */
        $pdo = $container->get('PDO.oxasql');
        $dilispec = (new Dilispec($pdo))->findAll(['TYPEDICO' => 'WS_SIRET']);
        
        $paramDB = array_column($dilispec, 'LIB01DICO', 'CODEDICO');
        $param = Utils::jsonDecode(
            // TODO : get _etc from container
            (string)file_get_contents((new ParseBase1())->getRep()->etc.'API/externe/WS_SIRET/Parametres.json', true),
            true
        );
        /** @phpstan-ignore-next-line */
        $env = $param[ $paramDB['TYPE_ENVIRONNEMENT'] ];
        return new SiretWsService(
            $env['base_url'],
            $env['base_url_auth'],
            $paramDB['LOGIN'],
            $paramDB['PASSWORD']
        );
    }

//_____________________________________________________________________________
// Accessor

    protected function getBaseUrl() :string
    {
        return $this->baseUrl;
    }

    protected function getBaseUrlAuth() :string
    {
        return $this->baseUrlAuth;
    }

    protected function getGuzzleClient() :Client
    {
        if ($this->client == null) {
            $this->client = new Client([
                //'base_uri' => $this->getBaseUrl(),
                // base_uri cannot be use due to the signature dependency for the full url
                'http_errors' => false,
            ]);
        }
        return $this->client;
    }

//_____________________________________________________________________________
// Token

    protected function getAuthTokenFromFile() :?string
    {
        $path = self::getFileTokenPath();
        // Init
        if (! file_exists($path)) {
            return null;
        }

        $size = filesize($path);
        if ($size === false) {
            return null;
        }
        
        $fp = fopen($path, "r");
        if ($fp === false) {
            throw new \Exception('Unreadable file "'.$path.'"');
        }

        $success = flock($fp, LOCK_SH); // Acquire "reading" lock
        if ($success === false) {
            throw new \Exception('Unlockable file "'.$path.'"');
        }
        
        $contents = fread($fp, $size);
        if ($contents === false) {
            $contents = null;
        }

        fclose($fp); // The lock is released also by fclose(), or when stream is garbage collected.
        return $contents;
    }

    protected static function getFileTokenPath() :string
    {
        return Fichier::getDirTmp() . '/WS_SIRET.token';
    }

    protected function setAuthTokenToFile(string $token) :void
    {
        $path = self::getFileTokenPath();

        // Init
        if (! file_exists($path)) {
            touch($path);
        }
        $fp = fopen($path, "w");
        if ($fp === false) {
            throw new \Exception('Unwritable file "'.$path.'"');
        }
        $success = flock($fp, LOCK_EX); // Acquire "writing" lock
        if ($success === false) {
            throw new \Exception('Unlockable file "'.$path.'"');
        }
        $success = fwrite($fp, $token, strlen($token));
        if ($success === false) {
            throw new \Exception('Unwritable file "'.$path.'"');
        }

        fclose($fp); // The lock is released also by fclose(), or when stream is garbage collected.
    }


//_____________________________________________________________________________
// Process

    protected function getAuthToken(bool $usecache = true) :string
    {
        // Case : get from cache
        if ($usecache) {
            $token = $this->getAuthTokenFromFile();
            if ($token !== null) {
                return $token;
            }
        }

        $response = $this->getGuzzleClient()
            ->send($request = new Request('POST', $this->getBaseUrlAuth() . '/token', [
                'Authorization' =>
                    'Basic '
                    . base64_encode($this->login . ':' . $this->password)
            ], 'grant_type=client_credentials'));

        if (! $this->_isValidHttpCodeStatus($response->getStatusCode())) {
            throw new ProvigisWsResponseException($request, $response);
        }
        
        /** @_var object{access_token:string,expires_in:int} */
        /** @var \stdClass */
        $payload = Utils::jsonDecode($response->getBody());

        $access_token = $payload->access_token ?? null;

        // Validate payload
        if (! $this->isValidToken($access_token)) {
            throw new ProvigisWsResponseBodyException($payload, 'access_token');
        }

        // Update cache
        $this->setAuthTokenToFile($access_token);
        return $access_token;
    }

    /**
     * @see https://www.sirene.fr/static-resources/htm/siret_unitaire_variables_reponse_311.html
     * @_return SearchResultItem[]
     * @return \stdClass[]
     */
    public function search(string $siretQuery, string $rsQuery) :array
    {
        $response = $this->getGuzzleClient()
            ->send($request = new Request(
                'GET',
                $this->getBaseUrl() . '/siret?'. http_build_query([
                    // Search by Siret or Raison Sociale
                    // 'q' => 'denominationUniteLegale:"oxalys technologie"~4',
                    // 'q' => 'denominationUniteLegale:oxalys~2 AND denominationUniteLegale:technologies~2',
                    'q' => implode(' OR ', array_filter([
                        !empty($siretQuery) ? 'siret:*'.$siretQuery . '*' : null,
                        !empty($rsQuery) ?
                            '('
                            . implode(
                                ' AND ',
                                array_map(function ($word) {
                                    return 'denominationUniteLegale:*'.$word . '*';
                                }, explode(' ', $rsQuery))
                            )
                            . ')' :
                            null,
                    ])),
                    'date' => date('Y-m-d'),
                ]),
                [
                    'Authorization' =>
                        'Bearer '
                        . $this->getAuthToken()
                ]
            ));
        
        // Case : no item found
        if ($response->getStatusCode() == 404) {
            return [];
        }

        if (! $this->_isValidHttpCodeStatus($response->getStatusCode())) {
            throw new ProvigisWsResponseException($request, $response);
        }
        
        /** @var \stdClass */
        $payload = Utils::jsonDecode($response->getBody());

        // Validate payload
        /** @phpstan-ignore-next-line */
        if (! is_array($payload->etablissements ?? null)) {
            throw new ProvigisWsResponseBodyException($payload, 'etablissements');
        }

        return (array) $payload->etablissements;
    }
    
//_____________________________________________________________________________
// Sub-routine

    /**
     * @param mixed $token
     */
    private function isValidToken($token) :bool
    {
        return is_string($token) && !empty($token);
    }

    private function _isValidHttpCodeStatus(int $code) :bool
    {
        return $code >= 200 && $code < 300 ;
    }
}
