<?php
namespace API\Edicom;

use API\Partenaire\BaseOAuth2Ws;
use API\Partenaire\Exception\WsInvalidResponse;
use GuzzleHttp\Psr7\MultipartStream;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Stream;
use GuzzleHttp\Utils;
use Helpers\Fichier\Fichier;
use Helpers\Outil\HTTP;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\SimpleCache\CacheInterface;

/**
 * @phpstan-type SubcripItem \stdClass&object{
 *     id :string,
 *     fullId :string,
 *     idSubscription :string,
 *     date: string,
 *     creationTime: string
 * }
 */
class EdicomWs extends BaseOAuth2Ws
{
    private string $username;
    private string $password;
    
    private string $group;
    private string $domain;
    private string $application;
    
//_____________________________________________________________________________
// Constructor

    public function __construct(
        string $username,
        string $password,
        string $login,
        string $secret,
        string $group,
        string $domain,
        string $application,
        CacheInterface $cache
    ) {
        parent::__construct(
            'https://accounts.edicomgroup.com/token',
            $login,
            $secret,
            $cache,
            'EDICOM'
        );
        $this->username = $username;
        $this->password = $password;
        
        $this->group = $group;
        $this->domain = $domain;
        $this->application = $application;
    }

    // public static function createFromConfig(ContainerInterface $container) :EdicomWs
    // {
    //     /** @var \PDO */
    //     $pdo = $container->get('PDO.oxasql');
    //     $dilispec = (new Dilispec($pdo))->findAll(['TYPEDICO' => 'EDICOM']);
        
    //     $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/EDICOM/Parametres.json',
    //     //         true
    //     //     ),
    //     //     true
    //     // );
    //     // $env = $param[ $paramDB['TYPE_ENVIRONNEMENT'] ];
    //     return new EdicomWs(
    //         $paramDB['USERNAME'],
    //         $paramDB['PASSWORD'],
    //         $paramDB['CLIENT_ID'],
    //         $paramDB['CLIENT_SECRET'],
    //         $paramDB['GROUP'],
    //         $paramDB['DOMAIN'],
    //         $paramDB['APP'],
    //         $container->get(CacheInterface::class),
    //     );
    // }
    
//_____________________________________________________________________________
// Auth

    /**
     * @return array<mixed>
     */
    protected function getAuthPayload() :array
    {
        return [
            'grant_type' => 'password',
            'username' => $this->username,
            'password' => $this->password,
        ] + parent::getAuthPayload();
    }

//_____________________________________________________________________________
// Accessor

    protected function getBaseUrl() :string
    {
        return 'https://ipaasgw.edicomgroup.com/api/v1/domain/'
            . $this->domain
            . '/application/'.$this->application.'/broker';
    }

//______________________________________________________________________________
// UBL retrieval

    /**
     * @return SubcripItem[]
     */
    public function listSubcription(string $schema)
    {
        $response = $this->send($request = $this->createRequest(
            'GET',
            '/subscription/list?schema=' . $schema,
        ));

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

    public function getFile(string $schema, string $id) :Stream
    {
        $response = $this->send($request = $this->createRequest(
            'GET',
            '/subscription?' . http_build_query([
                'schema' => $schema,
                'messageIds[]' => $id,
            ])
        ));
        
        if (! $this->isValidHttpCodeStatus($response->getStatusCode())) {
            throw new WsInvalidResponse($request, $response);
        }

        $file = null;
        EdicomWs::parseMultipart($response, function ($mapHeader, $content) use (&$file) {
            // $contentDispo = HTTP::parseContentDisposition(
            //     $mapHeader['content-disposition'][0] ?? ''
            // );

            $contentId = $mapHeader['content-id'][0] ?? null;

            if (empty($contentId)) {
                throw new \Exception('Missing content-id header');
            }

            // Ignore metadata file
            if ($contentId == '<root>') {
                return true; // keep reading
            }

            // Ignore non-xml file
            if (! str_ends_with($contentId, '.xml')) {
                return true; // keep reading
            }

            Fichier::touchDir($dir = Fichier::getDirTmp() . '/edicom_ubl');
            $b = file_put_contents(
                $filePath = $dir . '/' . $contentId,
                $content
            );
            if ($b === false) {
                throw new \Exception('Cannot write "'.$filePath.'"');
            }

            $file = $filePath;
            return false; // Stop reading
        });
        if ($file == null) {
            throw new \Exception('ubl not found in #'.$id);
        }

        return new Stream(Fichier::fopen($file, 'r'));
    }

    public function postDoc(string $schema, string $fileContent, string $filename) :void
    {
        
        $multipart = new MultipartStream([
            [
                'name'     => 'files',
                'contents' => $fileContent,
                'filename' => $filename,
                'headers' => ['Content-Type' => 'text/plain'],
            ]
        ]);
        
        $response = $this->send(
            $request = $this->createRequest(
                'POST',
                '/publish?' . http_build_query([
                    'schema' => $schema,
                    'duplicates' => 2,
                ])
            )
            ->withHeader('Content-Type', 'multipart/form-data; boundary='.$multipart->getBoundary())
            ->withBody($multipart)
        );
        
        if (! $this->isValidHttpCodeStatus($response->getStatusCode())) {
            throw new WsInvalidResponse($request, $response);
        }
    }

    /**
     * @param array<string> $id
     */
    public function confirmMessage(array $id) :void
    {
        $response = $this->send($request = $this->createRequest(
            'POST',
            '/subscription/confirm',
            array_values($id)
        ));

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

        $payload = Utils::jsonDecode($response->getBody());
        if (! in_array($payload, [1, -3, -4])) {
            throw new WsInvalidResponse($request, $response);
            /**
            [ 1 ] Message confirmed successfully
            [ -1 ] Error
            [ -2 ] Subscription not found
            [ -3 ] Status of the document not valid to be confirmed. Documents can only be confirmed when they are in subscribed, pending confirmation or locked status
            [ -4 ] Document already in confirmed status
            [ -12 ] Error registering the session
             */
        }
    }

//_____________________________________________________________________________
// Sub-routine

    protected function send(Request $request) :ResponseInterface
    {
        $resp = $this->getGuzzleClient()->send($request);
        // Try refreshing the token
        if (in_array($resp->getStatusCode(), [403, 401])) {
            $resp = $this->getGuzzleClient()->send($this->addToken($request, false));
        }
        return $resp;
    }

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

    protected function addToken(Request $request, bool $useCache = true) :Request
    {
        return $request->withHeader('Authorization', 'Bearer '.$this->getAuthToken($useCache));
    }

    public static function parseMultipart(?ResponseInterface $resp, callable $callback) :void
    {
        // Parse boundary from "multipart/mixed;boundary={boundary}"
        $boundary = trim(explode(
            '=',
            explode(
                ';',
                $contentType = $resp->getHeader('content-type')[0] ?? ''
            )[1] ?? ''
        )[1] ?? '');

        if (empty($boundary)) {
            throw new \Exception('Invalid content-type "'.$contentType.'"');
        }

        $stream = $resp->getBody();

        // DEBUG
        // $boundary = 'eIliKAebaYjsuy4AQR6AhShO_0sp1IGuC';
        // $stream = new Stream(fopen('E:\\Oxalys\\Oxalys_Broker\\versions\\xxbeta\\HAPlus\\oxaproc\\toto.txt', 'r'));


        $tokenBoundary = '--' . $boundary;

        //___________________
        // Read first boundary

        if (($read = $stream->read(strlen($tokenBoundary))) != $tokenBoundary) {
            throw new \Exception('Invalid body near '.$read);
        }
        
        while (true) {
            //___________________
            // Read Boundary ender

            $boundaryEnd = $stream->read(2);

            if ($boundaryEnd == '--') {
                return;
            }
            if ($boundaryEnd != "\r\n") {
                throw new \Exception('Invalid body'); // TODO : improve message
            }

            //___________________
            // Read header

            $mapHeader = HTTP::parseHeader(
                EdicomWs::readUntil($stream, "\r\n\r\n")
            );

            //___________________
            // Read content

            $content = EdicomWs::readUntil($stream, $tokenBoundary);
            
            if (($mapHeader['transfer-encoding'][0] ?? '') == 'base64') {
                $content = base64_decode($content);
            }

            if (! $callback($mapHeader, $content)) {
                return;
            }
        }
    }

    /**
     * Read until stoptoken is found or end of stream
     * Stream cursor is set after stoptoken
     * @return string data before the stopToken (not included)
     */
    public static function readUntil(StreamInterface $stream, string $stopToken) :string
    {
        $bufferSize = 1024;
        $buffer = '';
        $streamStart = $stream->tell();
        $offset = 0;
        $tokenLength = strlen($stopToken);

        while (true) {
            $read = $stream->read($bufferSize);

            // Case : End of stream
            if (empty($read)) {
                return $buffer;
            }

            $buffer .= $read;
            
            $pos = strpos($buffer, $stopToken, max($offset - $tokenLength, 0));

            $offset += strlen($read);

            // Case : Token not found -> keep reading
            if ($pos === false) {
                continue;
            }

            // Case : Token found
            // rewind steam to after the token
            $stream->seek($streamStart + $pos + $tokenLength);

            return substr($buffer, 0, $pos);
        }
    }
}
