<?php
namespace API\OnceForAll;

use API\Partenaire\Exception\WsInvalidResponse;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Utils;
use Helpers\Log\LogSysteme;
use Helpers\Outil\MemoizeTrait;
use Psr\Http\Message\ResponseInterface;
use Psr\SimpleCache\CacheInterface;

/**
 * https://connect.onceforall.com/documentation/index.html
 * _phpstan-type TokenResponse object{access_token:string,expires_in:int}
 * @phpstan-type TokenResponse \stdClass
 * _phpstan-type Pagination object{page:int,last:int}
 * @phpstan-type Pagination \stdClass
 * @phpstan-type Document \stdClass
 * _phpstan-type Document object{
 *     id:int,
 *     state:'NOT_APPLICABLE'|'VALID'|'ALMOST_EXPIRED'|'MISSING'|'AWAITING_GATHERING'|'AWAITING_VALIDATION'|'REJECTED'|'AWAITING_SIGNATURE'|'SIGNED'|'EXPIRED',
 *     type:object{
 *         name:string,
 *         code:string,
 *         familyCode:?string,
 *         country:string,
 *         dossier:string
 *     },
 *     depositDate:string,
 *     validityStartDate:string,
 *     validityEndDate:string,
 *     checksum:string,
 *     issueDate:string,
 *     downloadUrl:string
 * }
 * @phpstan-type Orga \stdClass
 * _phpstan-type Orga object{
 *     id:int,
 *     country:string,
 *     name:string,
 *     enabled:bool,
 *     depositor:bool,
 *     registrationNumber:string,
 *     address:object{
 *         addressLine:string,
 *         city:string,
 *         postCode:string,
 *     },
 *     identificationNumbers:object<mixed>,
 *     lastDocumentsUpdateDate:string
 * }
 * @phpstan-type SubcontractorInvite array{
 *     country: mixed,
 *     email: mixed,
 *     firstName: mixed,
 *     lastName: mixed,
 *     organisationName: mixed,
 *     phoneNumber: mixed,
 *     registrationNumber?: mixed,
 *     vatIdentificationNumber?: mixed,
 *     customMessage: string
 * }
 */
class OnceForAllWs
{
    private ?Client $client = null;
    private string $baseUrl; //= 'https://connect.staging.onceforall.com';
    private string $authUrl; //= 'https://identity.staging.onceforall.com/auth/realms/Connect/protocol/openid-connect/token';

    private string $clientId;
    private string $clientSecret;

    private CacheInterface $cache;

    use MemoizeTrait;

//_____________________________________________________________________________
// Constructor

    public function __construct(
        string $baseUrl,
        string $authUrl,
        string $clientId,
        string $clientSecret,
        CacheInterface $cache
    ) {
        $this->baseUrl = trim($baseUrl, '/');
        $this->authUrl = trim($authUrl, '/');
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->cache = $cache;
    }

//_____________________________________________________________________________
// Accessor

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

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

//_____________________________________________________________________________
// Cache

    /**
     * @return TokenResponse|null
     */
    protected function getAuthTokenFromCache() :?object
    {
        if (!$this->cache->has('OFA_TOKEN_AUTH')) {
            return null;
        }
        return $this->cache->get('OFA_TOKEN_AUTH');
    }

    /**
     * @param TokenResponse $token
     */
    protected function setAuthTokenToCache(object $token) :self
    {
        $b = $this->cache->set('OFA_TOKEN_AUTH', $token);
        if ($b === false) {
            LogSysteme::prefixErrorLog((new \Exception('Cache failure'))->__toString());
        }
        return $this;
    }

//_____________________________________________________________________________
// Authentication

    /**
     * @return TokenResponse
     */
    public function getAuthToken(bool $usecache = true) :object
    {
        // Case : get from cache
        if ($usecache) {
            $token = $this->getAuthTokenFromCache();
            if ($token !== null) {
                return $token;
            }
        }

        $response = $this->getGuzzleClient()
            ->send($request = new Request(
                'POST',
                $this->getAuthUrl(),
                [ 'Content-Type' => 'application/x-www-form-urlencoded' ],
                http_build_query([
                    'grant_type' => 'client_credentials',
                    'client_id' => $this->clientId,
                    'client_secret' => $this->clientSecret,
                ])
            ));

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

        // Validate payload
        /** @phpstan-ignore-next-line */
        if (! $this->_isValidToken($payload->access_token ?? null)) {
            throw new WsInvalidResponse($request, $response);
        }
        if (! is_int($payload->expires_in ?? null)) {
            throw new WsInvalidResponse($request, $response);
        }

        // Update cache
        $this->setAuthTokenToCache($payload);
        return $payload;
    }

//_____________________________________________________________________________
// Retriever

    /**
     * @param array{
     *     country:string,
     *     registrationNumber:string,
     *     groupNumber:string,
     *     vatIdentificationNumber:string,
     *     page:int,
     *     size:int
     * } $filter
     * where registrationNumber, groupNumber, vatIdentificationNumber are a comma-separated list
     * and country is ISO 3166-1 alpha 2
     *
     * @return array<Orga>
     */
    // public function searchOrga(array $filter) :array
    // {
    //     $response = $this->getGuzzleClient()
    //         ->send($request = $this->setToken(new Request(
    //             'GET',
    //             '/organisations' . http_build_query($filter)
    //         )));

    //     if (! $this->_isValidHttpCodeStatus($response->getStatusCode())) {
    //         throw new WsInvalidResponse($request, $response);
    //     }
        
    //     $payload = Utils::jsonDecode($response->getBody());

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

    /**
     * Summary of getSubcontractors
     * @param array{
     *     page?:int,
     *     size?:int,
     *     country?:string,
     *     registrationNumber?:string,
     * } $filter (where
     * - size: default 10; max 1500
     * - registrationNumber is a comma-separated list
     * )
     * _return object{pagination:Pagination,list:array<Orga>}
     * @return \stdClass
     */
    public function listSubcontractor(int $orgaId, array $filter) :object
    {
        $response = $this->sendWithAuth($request = new Request(
            'GET',
            $this->getBaseUrl()
            . '/api/organisations/'.$orgaId.'/subcontractors'
            . '?' . http_build_query($filter)
        ));
        
        /** @var array<Orga> */
        $payload = Utils::jsonDecode($response->getBody());

        // Validate payload
        /** @phpstan-ignore-next-line */
        if (! is_array($payload)) {
            throw new WsInvalidResponse($request, $response);
        }
        
        // Get pagination
        $lastPageUrl = $this->parseLinkHeader(
            $response->getHeader('Link')[0] ?? ''
        )['first'];
        parse_str((string)parse_url($lastPageUrl, PHP_URL_QUERY), $query);

        return (object) [
            'pagination' => (object) [
                'page' => $filter['page'] ?? 0,
                'last' => intval($query['page']),
            ],
            'list' => $payload,
        ];
    }

    /**
     * @return string[]
     */
    private function parseLinkHeader(string $value) :array
    {
        $res = [];
        $data = array_column(
            array_map(function ($item) {
                return explode(';', $item);
            }, explode(',', $value)),
            0,
            1
        );
        foreach ($data as $k => $v) {
            $res[ trim(substr(trim((string)$k), 4) /* remove rel="" */, '"') ] = trim($v, ' <>');
        }
        return $res;
    }

    /**
     * @param array{
     *     code?:array<string|int>,
     * } $filter
     * @return array<Document>
     */
    public function listSubcontractorDocument(
        int $orgaId,
        int $subcontractorId,
        array $filter
    ) :array {
        $response = $this->sendWithAuth($request = new Request(
            'GET',
            $this->getBaseUrl()
            . '/api/organisations/'.$orgaId.'/subcontractors/'.$subcontractorId.'/documents'
            . '?' . http_build_query(array_filter([
                'code' => implode(',', $filter['code'] ?? []),
            ]))
        ));
        
        /** @var array<Document> */
        $payload = Utils::jsonDecode($response->getBody());

        // Validate payload
        /** @phpstan-ignore-next-line */
        if (! is_array($payload)) {
            throw new WsInvalidResponse($request, $response);
        }
        
        return $payload;
    }

    public function getDocumentFile(string $url) :ResponseInterface
    {
        return $this->sendWithAuth(new Request('GET', $url));
    }

//_____________________________________________________________________________
// Action

    /**
     * @param int $orgaId
     * _param SubcontractorInvite $subcontractor
     * @param array<mixed> $subcontractor
     */
    public function inviteSubcontractor(
        int $orgaId,
        array $subcontractor
    ) :void {
        $response = $this->sendWithAuth(new Request(
            'POST',
            $this->getBaseUrl() . '/api/organisations/'.$orgaId.'/subcontractors',
            ['Content-Type' => 'application/json'],
            Utils::jsonEncode($subcontractor)
        ));
    }

//_____________________________________________________________________________
// 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 ;
    }

    /**
     * @return Request
     */
    protected function setToken(Request $request, bool $useCache = true) :Request
    {
        /** @var Request */
        $request = $request->withHeader(
            'Authorization',
            'Bearer '.($this->getAuthToken($useCache)->access_token ?? '')
        );
        return $request;
    }

    protected function sendWithAuth(Request $request) :ResponseInterface
    {
        $client = $this->getGuzzleClient();
        $response = $client->send($request = $this->setToken($request));

        // Relance with new token
        if ($response->getStatusCode() == 401) {
            $response = $client->send($request = $this->setToken($request, false));
        }

        if (! $this->_isValidHttpCodeStatus($response->getStatusCode())) {
            throw new WsInvalidResponse($request, $response);
        }

        return $response;
    }
}
