<?php
namespace API\DirectPlus;

use API\Partenaire\BaseWs;
use API\Partenaire\Exception\WsInvalidResponse;
use Entity\Model\Dilispec;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\MultipartStream;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Utils;
use Helpers\Fichier\Fichier;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\SimpleCache\CacheInterface;
use Helpers\Session\ParseBase1;

/**
 * @see https://directplus.documentation.dnb.com/DataBlocks.html
 */
class DirectPlusWs extends BaseWs
{
    private ?Client $client = null;
    private string $baseUrl; //= 'https://plus.dnb.com';

    private string $login;
    private string $password;

    private string $registerRef;

    private CacheInterface $cache;

//_____________________________________________________________________________
// Constructor

    public function __construct(
        string $baseUrl,
        string $login,
        string $secret,
        string $registerRef,
        CacheInterface $cache
    ) {
        $this->baseUrl = trim($baseUrl, '/');
        $this->login = $login;
        $this->password = $secret;
        $this->registerRef = $registerRef;
        $this->cache = $cache;
    }

    public static function createFromConfig(ContainerInterface $container) :DirectPlusWs
    {
        /** @var \PDO */
        $pdo = $container->get('PDO.oxasql');
        $dilispec = (new Dilispec($pdo))->findAll(['TYPEDICO' => 'ALTADIR']);
        
        $paramDB = array_column($dilispec, 'LIB01DICO', 'CODEDICO');
        /** @var array<mixed> */
        $param = Utils::jsonDecode(
            // TODO : get _etc from container
            (string) @file_get_contents(
                (new ParseBase1())->getRep()->etc.'API/externe/ALTADIR/Parametres.json',
                true
            ),
            true
        );
        $env = $param[ $paramDB['TYPE_ENVIRONNEMENT'] ];
        return new DirectPlusWs(
            $env['base_url'],
            $paramDB['APIKEY'],
            $paramDB['APISECRET'],
            $paramDB['REGISTER_REF'],
            $container->get(CacheInterface::class)
        );
    }

//_____________________________________________________________________________
// Accessor

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

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

//_____________________________________________________________________________
// Cache

    protected function getAuthTokenFromCache() :?string
    {
        $expiry = $this->cache->get('ALTARES_TOKEN_AUTH_EXPIRY');
        // Case : token has expired
        if ($expiry != null
            && new \DateTime('@'.$expiry) < new \DateTime()
        ) {
            $this->cache->deleteMultiple([
                'ALTARES_TOKEN_AUTH',
                'ALTARES_TOKEN_AUTH_EXPIRY',
            ]);
            return null;
        }
        return $this->cache->get('ALTARES_TOKEN_AUTH');
    }

    protected function setAuthTokenToCache(string $token, \DateTime $expiry) :self
    {
        $this->cache->set('ALTARES_TOKEN_AUTH', $token);
        $this->cache->set('ALTARES_TOKEN_AUTH_EXPIRY', $expiry->getTimestamp());
        return $this;
    }

//_____________________________________________________________________________
// Process

    /**
     * @return string
     */
    public function getAuthToken(bool $use_cache = true) :string
    {
        // Case : get from cache
        if ($use_cache) {
            $token = $this->getAuthTokenFromCache();
            if ($token !== null) {
                return $token;
            }
        }
        
        $response = $this->getGuzzleClient()
            ->send($request = new Request(
                'POST',
                $this->getBaseUrl() . '/v2/token',
                [
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json',
                    'Authorization' => 'Basic ' . base64_encode(
                        $this->login . ':' . $this->password
                    ),
                ],
                Utils::jsonEncode([
                    'grant_type' => 'client_credentials'
                ])
            ));

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

        // Validate payload
        $token = $payload->access_token ?? null;
        if (empty($token)) {
            throw new \Exception('Invalid token response "'.json_encode($payload).'"');
        }

        $expiry = $payload->expiresIn ?? null;
        if (!is_int($expiry)) {
            throw new \Exception('Invalid token response "'.json_encode($payload).'"');
        }

        // Update cache
        $this->setAuthTokenToCache($payload->access_token, new \DateTime('+'.$expiry.' second'));
        return $payload->access_token;
    }

    /**
     * @param array{
     *     name?:string,
     *     duns?:string,
     *     registrationNumber?:string,
     *     postalCode?:string
     * } $query
     * @return array{count:?int,hasMore:bool,list:array<\stdClass>}
     */
    public function search(string $countryCode, array $query) :array
    {
        $response = $this->send($request = $this->createRequest(
            'GET',
            '/v1/match/cleanseMatch?'
            .http_build_query(
                [
                    'countryISOAlpha2Code' => $countryCode,
                    'candidateMaximumQuantity' => 25, // Maximum limit is 25, except for a nationwide search in US, CA - Maximum limit is 100.
                ]
                + (strlen($query['name'] ?? '') > 2 ? [
                    'name' => substr($query['name'], 0, 60)
                ] : [])
                + $this->addOption('duns', $query, 9)
                + $this->addOption('postalCode', $query, 10)
                + $this->addOption('addressLocality', $query, 50)
                + $this->addOption('registrationNumber', $query, 99)
            )
        ));

        // Case : No Match found for the given input criteria
        if ($response->getStatusCode() == 404
            && str_starts_with(
                $response->getHeader('Content-Type')[0],
                'application/json'
            )
        ) {
            $payload = Utils::jsonDecode($response->getBody());
            if (($payload->error->errorCode ?? null) == 20505) {
                return [
                    'count' => 0,
                    'hasMore' => false,
                    'list' => [],
                ];
            }
        }
        
        if (! $this->isValidHttpCodeStatus($response->getStatusCode())) {
            throw new WsInvalidResponse($request, $response);
        }

        /** @var object{matchCandidates:array<object{organization:object}>} */
        /** @var \stdClass */
        $payload = Utils::jsonDecode($response->getBody());

        return [
            // 'count' => intval($payload->candidatesMatchedQuantity ?? 0),
            // Note : candidatesMatchedQuantity does not return the total just the total returned
            'count' => null,
            'hasMore' => count($payload->matchCandidates) >= 25, // Assume there is always a 26th result even if it's not true
            'list' =>
                array_map(function ($item) {
                    return $item->organization;
                }, $payload->matchCandidates),
        ];
    }

    /**
     * @return \stdClass|null
     */
    public function companyInfo(string $duns, bool $level2 = false) :?object
    {
        $response = $this->send($request = $this->createRequest(
            'GET',
            '/v1/data/duns/' . urlencode($duns) . '?'
            . http_build_query([
                'blockIDs' => $level2 ? 'companyinfo_L1_v1' : 'companyinfo_L2_v1',
            ])
        ));

        // Case : invalid duns
        if (in_array($response->getStatusCode(), [400, 410])) {
            /** @var \stdClass */
            $payload = Utils::jsonDecode($response->getBody());
            if (in_array($payload->error->errorCode, [
                10003, /* Supplied DUNS number format is invalid */
                40002, /* The requested D-U-N-S Number cannot be returned as it has been deleted. */
            ])) {
                return null;
            }
        }

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

        /** @var object{organization:object} */
        /** @var \stdClass */
        $payload = Utils::jsonDecode($response->getBody());

        return $payload->organization;
    }

//_____________________________________________________________________________
// Monitoring

    /** @var resource */
    private static $fp = null;

    /**
     * @throws \API\Partenaire\Exception\WsInvalidResponse
     * @return \stdClass{
     *     type:string,
     *     organization:object{duns:string},
     *     elements:array<object{element:string,previous:mixed,current:mixed}>
     * }|null
     */
    public function getNotificationTestData() :?object
    {
        if (self::$fp === null) {
            self::$fp = Fichier::fopen(__DIR__."/../../../tests/ExtApi/DirectPlus/NOTIF_SAMPLE.txt", "r");
        }
        $line = fgets(self::$fp);

        if ($line === false) {
            return null;
        }
        /** @var \stdClass */
        return Utils::jsonDecode($line);
    }
    
    /**
     * @throws \API\Partenaire\Exception\WsInvalidResponse
     * @return \stdClass{
     *     type:string,
     *     organization:object{duns:string},
     *     elements:array<object{element:string,previous:mixed,current:mixed}>
     * }|null
     */
    public function getNotification() :?object
    {
        
        $response = $this->send($request = $this->createRequest(
            'GET',
            '/v1/monitoring/registrations/' . $this->registerRef
            . '/notifications?maxNotifications=1'
            // Note : call may timeout if maxNotifications too large,
            // we play it safe and call it one by one
        ));

        // Case : no notif
        if ($response->getStatusCode() == 204/* No Content */) {
            return null;
        }

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

        /** @var \stdClass{notifications:array<object{
         *     type:string,
         *     organization:object{duns:string},
         *     elements:array<object{element:string,previous:mixed,current:mixed}>
         * }>} */
        $payload = Utils::jsonDecode($response->getBody());
        
        return $payload->notifications[ 0 ] ?? null;
    }

    public function monitoringAddSubject(string $duns) :void
    {
        $response = $this->send($request = $this->createRequest(
            'POST',
            '/v1/monitoring/registrations/' . $this->registerRef . '/subjects/'.$duns
        ));

        // Case : ignore "The requested duns already exists" (error 21012)
        if ($response->getStatusCode() == 400) {
            /** @var \stdClass */
            $payload = Utils::jsonDecode($response->getBody());
            if ($payload->error->errorCode == '21012') {
                return;
            }
        }

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

    public function monitoringRemoveSubject(string $duns) :void
    {
        $response = $this->send($request = $this->createRequest(
            'DELETE',
            '/v1/monitoring/registrations/' . $this->registerRef . '/subjects/'.$duns
        ));

        // TODO : ignore "does not exist" error

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

    /**
     * @param array<string|int> $listDuns
     */
    public function monitoringAddSubjectList(array $listDuns) :void
    {
        $multipart = new MultipartStream([
            [
                'name'     => 'addSubjectIDs',
                'contents' => implode("\r\n", $listDuns)."\r\n",
                'filename' => 'toto.txt',
                'headers' => ['Content-Type' => 'text/plain'],
            ]
        ]);
        
        /** @var Request */
        $request = $this
            ->createRequest(
                'PATCH',
                '/v1/monitoring/registrations/' . $this->registerRef . '/subjects'
            )
            // We force the Content-type here instead of letting Guzzle handle it since a content-type json is already forced
            ->withHeader('Content-Type', 'multipart/form-data; boundary='.$multipart->getBoundary())
            ->withBody($multipart);
        
        $response = $this->send($request);

        if (! $this->isValidHttpCodeStatus($response->getStatusCode())) {
            throw new WsInvalidResponse($request, $response);
        }
        /** @var \stdClass */
        $payload = Utils::jsonDecode($response->getBody());
        
        if (($payload->information->code ?? null) != '21108') {
            throw new WsInvalidResponse($request, $response);
        }
    }

//_____________________________________________________________________________
// Sub-routine

    /**
     * @param array<mixed> $data
     * @return string[]
     */
    private function addOption(string $key, array $data, int $max)
    {
        return (!empty($data[ $key ]) ? [
            $key => substr($data[ $key ], 0, $max),
        ] : []);
    }

    protected function send(Request $request) :ResponseInterface
    {
        return $this->getGuzzleClient()
            ->send($request);
    }

    /**
     * @param ?array<mixed> $jsonPayload
     */
    protected function createRequest(
        string $method,
        string $path,
        ?array $jsonPayload = null
    ) :Request {
        return new Request(
            $method,
            $this->getBaseUrl() . $path,
            [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer '.$this->getAuthToken(),
            ]
            + (! empty($jsonPayload) ? ['Content-Type' => 'application/json'] : []),
            ! empty($jsonPayload) ? Utils::jsonEncode($jsonPayload) : null
        );
    }

    /**
     * @param array<mixed> $payload
     */
    protected function _createJsonRequest(
        string $method,
        string $path,
        $payload
    ) :Request {
        return new Request(
            $method,
            $this->getBaseUrl() . $path,
            [
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
            Utils::jsonEncode($payload)
        );
    }
}
