<?php
namespace API\Provigis;

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\Log\LogSysteme;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * phpstan-type TokenResponse object{oauth_token:string,oauth_token_secret:string}
 * TODO : why phpstan-type does not work ?
 */
class ProvigisWsService
{
    private ?Client $_client = null;
    private string $_baseUrl; //= 'https://api.provigis.com/api';

    private string $_consumerKey;
    private string $_secret;
    private string $_login;
    private string $_password;

    private string $_buyer_id;

    /**
     * @var object{oauth_token:string,oauth_token_secret:string}|null
     */
    private ?object $_auth_token = null;
    private ?object $_access_token = null;

    private CacheInterface $_cache;

//_____________________________________________________________________________
// Constructor

    public function __construct(
        string $baseUrl,
        string $consumerKey,
        string $consumerSecret,
        string $login,
        string $password,
        string $buyer_id,
        CacheInterface $cache
    ) {
        $this->_baseUrl = trim($baseUrl, '/');
        $this->_consumerKey = $consumerKey;
        $this->_secret = $consumerSecret;
        $this->_login = $login;
        $this->_password = $password;
        $this->_buyer_id = $buyer_id;
        $this->_cache = $cache;
    }

    public static function createFromConfig(ContainerInterface $container) :ProvigisWsService
    {
        /** @var \PDO */
        $pdo = $container->get('PDO.oxasql');
        $dilispec = (new Dilispec($pdo))->findAll(['TYPEDICO' => 'PROVIGIS']);
        
        $paramDB = array_column($dilispec, 'LIB01DICO', 'CODEDICO');
        $param = Utils::jsonDecode(
            // TODO : get _etc from container
            (string)file_get_contents($_SESSION['_etc'].'API/externe/PROVIGIS/Parametres.json', true),
            true
        );
        /** @phpstan-ignore-next-line */
        $env = $param[ $paramDB['TYPE_ENVIRONNEMENT'] ];
        return new ProvigisWsService(
            $env['base_url'],
            $paramDB['CONSUMER_KEY'],
            $paramDB['CONSUMER_SECRET'],
            $paramDB['USER'],
            $paramDB['MDP'],
            $paramDB['BUYER_ID'],
            $container->get('wincache')
        );
    }

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

    /**
     * @return object{oauth_token:string,oauth_token_secret:string}
     */
    protected function getAuthTokenFromCache() :?object
    {
        if (!$this->_cache->has('PROGEDIS_TOKEN_AUTH')) {
            return null;
        }
        return $this->_cache->get('PROGEDIS_TOKEN_AUTH');
    }

    protected function setAuthTokenToCache(object $token) :self
    {
        $b = $this->_cache->set('PROGEDIS_TOKEN_AUTH', $token);
        if ($b === false) {
            LogSysteme::prefixErrorLog((new \Exception('Cache failure'))->__toString());
        }
        return $this;
    }

    /**
     * @return object{oauth_token:string,oauth_token_secret:string}
     */
    protected function getAccessTokenFromCache() :?object
    {
        if (!$this->_cache->has('PROGEDIS_TOKEN_ACCESS')) {
            return null;
        }
        return $this->_cache->get('PROGEDIS_TOKEN_ACCESS');
    }

    protected function setAccessTokenToCache(object $token) :self
    {
        $b = $this->_cache->set('PROGEDIS_TOKEN_ACCESS', $token);
        if ($b === false) {
            LogSysteme::prefixErrorLog((new \Exception('Cache failure'))->__toString());
        }
        return $this;
    }

//_____________________________________________________________________________
// Process

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

        $response = $this->getGuzzleClient()
            ->send($this->_addOAuthInfo(
                $request = $this->_createRequest('GET', '/requestToken'),
                (object) [
                    'oauth_token' => '',
                    'oauth_token_secret' => '',
                ] // Token must be empty for /requestToken
            ));

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

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

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

    /**
     * @return object{oauth_token:string,oauth_token_secret:string}
     */
    public function getAccessToken(bool $use_cache = true) :object
    {
        // Case : get from cache
        if ($use_cache) {
            $token = $this->getAccessTokenFromCache();
            if ($token !== null) {
                return $token;
            }
        }
        
        $response = $this->getGuzzleClient()
            ->send($this->_addOAuthInfoAccess(
                $request = $this->_createRequest('GET', '/accessToken'),
                $this->getAuthToken()
            ));

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

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

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

        // Update cache
        $this->setAccessTokenToCache($payload);

        return $payload;
    }

    /**
     * @return object{suppliers:object}
     */
    public function getSupplierList() :object
    {
        $response = $this->_sendBaseRequest($request = $this->_createRequest(
            'GET',
            '/buyers/'.$this->_buyer_id.'/suppliers?'
                . http_build_query(['level' => 1,'max' => 5000])
        ));

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

        // Validate payload
        /** @phpstan-ignore-next-line */
        if (! is_object($payload->suppliers ?? null)) {
            throw new ProvigisWsResponseBodyException($payload, 'suppliers');
        }
        /** @phpstan-ignore-next-line */
        foreach ($payload->suppliers as $supplier_id => $supplier) {
            if (!is_string($supplier->siret ?? null)) {
                throw new ProvigisWsResponseBodyException(
                    $payload,
                    implode('.', ['suppliers', $supplier_id,'siret'])
                );
            }
        }
        
        return $payload;
    }

    public function getSupplierInfo(string $supplier_id) :?object
    {
        $response = $this->_sendBaseRequest($request = $this->_createRequest(
            'GET',
            '/buyers/'.$this->_buyer_id.'/suppliers/'.$supplier_id
        ));

        if ($response->getStatusCode() == 412) {
            $error = Utils::jsonDecode($response->getBody());
            // Case : Invalid parameter SIRET or ID
            /** @phpstan-ignore-next-line */
            if (($error->code ?? null) == 4019) {
                return null;
            }
        }

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

        // Validate payload
        $docPathArray = ['suppliers',$supplier_id,'documents'];
        /** @phpstan-ignore-next-line */
        if (! is_object($payload->suppliers->$supplier_id->documents ?? null)) {
            throw new ProvigisWsResponseBodyException(
                $payload,
                implode('.', $docPathArray)
            );
        }

        /** @phpstan-ignore-next-line */
        $docNode = $payload->suppliers->$supplier_id->documents;

        // Validate doc "legal"
        if (isset($docNode->legal)) {
            foreach ($docNode->legal as $index => $doc) {
                if (! is_string($doc->name ?? null)) {
                    throw new ProvigisWsResponseBodyException(
                        $payload,
                        implode('.', array_merge($docPathArray, ['legal',$index,'name']))
                    );
                }
                if (! $this->_isValidDocStatus($doc->status ?? null)) {
                    throw new ProvigisWsResponseBodyException(
                        $payload,
                        implode('.', array_merge($docPathArray, ['legal',$index,'status']))
                    );
                }
                // validity is not set for "status" = "missing"
                // if( !is_string($doc->validity ?? null) )// TODO : should be date Y-m-d
                //     throw new ProvigisWsResponseBodyException(
                //         $payload,
                //         implode('.', array_merge($docPathArray, [$index,'validity']))
                //     );
            }
        }
        return $payload;
    }

    public function downloadDoc(string $docId) :ResponseInterface
    {
        $response = $this->_sendBaseRequest($request = $this->_createRequest(
            'GET',
            '/download/'.$docId
        ));

        if (! $this->_isValidHttpCodeStatus($response->getStatusCode())) {
            LogSysteme::prefixErrorLog('Document #'.$docId.' no available ('.$response->getStatusCode().')');
        }

        return $response;
    }

    public function createSupplier(
        string $siret,
        string $custom_code,
        string $label,
        string $contact_label,
        string $contact_email,
        string $contact_cellphone,
        string $contact_phone,
        string $contact_fonction
    ) :string {
        $response = $this->_sendBaseRequest($request = $this->_createRequest(
            'POST',
            '/buyers/'.$this->_buyer_id.'/suppliers?'
            . http_build_query(array_filter([
                'supplier_siret' => $siret,

                'supplier_contact_civility' => 0,
                'supplier_contact_cellphone' => str_replace('.', ' ', $contact_cellphone),
                'supplier_contact_email' => $contact_email,
                'supplier_contact_firstname' => '',
                'supplier_contact_function' => $contact_fonction,
                'supplier_contact_lastname' => $contact_label,
                'supplier_contact_phone' => str_replace('.', ' ', $contact_phone),

                'supplier_custom_code' => $custom_code,
                'supplier_name' => $label,
            ]))
        ));

        if (! $this->_isValidHttpCodeStatus($response->getStatusCode())) {
            throw new ProvigisWsResponseException($request, $response);
        }
        
        /** @var object{suppliers:object} */
        $payload = Utils::jsonDecode($response->getBody());
        
        // Validate payload
        /** @phpstan-ignore-next-line */
        if (! is_int($payload->supplier->id)) {
            throw new ProvigisWsResponseBodyException(
                $payload,
                'supplier.id'
            );
        }
        /** @phpstan-ignore-next-line */
        return (string) $payload->supplier->id;
    }

    public function removeSupplier(string $provigisId) :void
    {
        $response = $this->_sendBaseRequest($request = $this->_createRequest(
            'DELETE',
            '/buyers/'.$this->_buyer_id.'/suppliers/'.$provigisId
        ));

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

    

//_____________________________________________________________________________
// Sub-routine

    private function _sendBaseRequest(Request $request) :ResponseInterface
    {
        // TODO : handle 401 stale token
        return $this->getGuzzleClient()
            ->send($this->_addOAuthInfo(
                $request,
                $this->getAccessToken()
            ));
    }

    private function _generateNonce() :string
    {
        return md5(microtime().rand(0, 1000));
    }

    private function _getSignature(Request $request, string $auth_token_secret) :string
    {
        $sign = $this->_getSignaturePayload($request);
        return base64_encode(
            hash_hmac('sha1', $sign, $this->_secret . '&' . $auth_token_secret, true)
        );
    }

    /**
     * Signature specific to /accessToken
     */
    private function _getSignatureAccess(Request $request, string $auth_token_secret) :string
    {
        $sign = $this->_getSignaturePayload($request);
        return base64_encode(
            hash_hmac('sha1', $sign, implode('&', [
                $this->_secret , $auth_token_secret, $this->_password
            ]), true)
        );
    }

    private function _getSignaturePayload(Request $request) :string
    {
        $uri = $request->getUri();
        return implode('&', [
            $request->getMethod(),
            \urlencode($uri->getScheme() . '://' . $uri->getHost() . $uri->getPath()),
            \urlencode($request->getUri()->getQuery()),
        ]);
    }

    private function _addOAuthInfo(Request $request, object $token) :Request
    {
        $uri = $request->getUri();
        $prev_query = $uri->getQuery();

        parse_str($prev_query, $param);
        $param += $this->_getOAuthInfo($token);

        // Param key MUST be sorted (alpha ASC) for the signature calculation
        ksort($param);

        $uri = $uri->withQuery(http_build_query($param, '', '&', PHP_QUERY_RFC3986));

        return $this->_addSignature(
            $request->withUri($uri),
            /** @phpstan-ignore-next-line */
            $token->oauth_token_secret
        );
    }

    /**
     * Specific for /accessToken
     */
    private function _addOAuthInfoAccess(Request $request, object $token) :Request
    {
        $uri = $request->getUri();
        $param = $this->_getOAuthInfo($token) + [
            'oauth_login' => $this->_login,
        ];

        // Param key MUST be sorted (alpha ASC) for the signature calculation
        ksort($param);
        $uri = $uri->withQuery(http_build_query($param));
        $request = $request->withUri($uri);

        $uri = $uri->withQuery($uri->getQuery() . '&' . http_build_query([
            'oauth_signature' => $this->_getSignatureAccess(
                $request,
                /** @phpstan-ignore-next-line */
                $token->oauth_token_secret
            ),
        ]));
        return $request->withUri($uri);
    }

    /**
     * param object{oauth_token:string,oauth_token_secret:string} $token
     * @param object $token
     * @return array<string,string>
     */
    private function _getOAuthInfo(object $token) :array
    {
        return [
            'oauth_consumer_key' => $this->_consumerKey,
            'oauth_nonce' => $this->_generateNonce(),
            //'oauth_signature' => ..., added after
            'oauth_signature_method' => 'HMAC-SHA1',
            'oauth_timestamp' => (string) time(),
            /** @phpstan-ignore-next-line */
            'oauth_token' => $token->oauth_token,
            'oauth_version' => '1.0',
        ];
    }

    private function _addSignature(Request $request, string $auth_token_secret) :Request
    {
        $uri = $request->getUri();
        $uri = $uri->withQuery($uri->getQuery() . '&' . http_build_query([
            'oauth_signature' => $this->_getSignature($request, $auth_token_secret),
        ]));
        return $request->withUri($uri);
    }

    /**
     * @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 ;
    }

    /**
     * @param mixed $status
     */
    private function _isValidDocStatus($status) :bool
    {
        // Silent error
        if (! in_array($status, ['up_to_date','to_verify','to_update','missing'])) {
            LogSysteme::prefixErrorLog('Status "'.$status.'" is not suppported');
        }
        return is_string($status);
    }

    protected function _createRequest(string $method, string $path) :Request
    {
        return new Request(
            $method,
            $this->getBaseUrl() . $path,
            [ 'Accept' => 'application/json' ]
        );
    }
}
