<?php
/*! Liseuse PDF — SDK PHP 1.1.0 · https://liseuse-pdf.com/docs/sdk-serveur · MIT
 * PHP 7.4+ avec curl. Côté serveur uniquement : la clé API ne doit jamais atteindre le navigateur.
 *
 *   require 'LiseusePDF.php';
 *   $liseuse = new LiseusePDF(getenv('LISEUSE_CLE_API'));
 *   $lien = $liseuse->creerLien('liseuse-pdf.com:inklura-escales-n12', 1);
 *   echo $lien['iframe'];
 *
 *   // webhook :
 *   $evenement = LiseusePDF::verifierWebhook(file_get_contents('php://input'), $_SERVER['HTTP_LISEUSE_SIGNATURE'] ?? '', getenv('LISEUSE_SECRET_WEBHOOK'));
 */

class LiseusePDFException extends RuntimeException
{
    public $status;
    public $codeErreur;

    public function __construct($status, $codeErreur, $message)
    {
        parent::__construct($message, (int) $status);
        $this->status = (int) $status;
        $this->codeErreur = $codeErreur;
    }
}

class LiseusePDF
{
    private $cleApi;
    private $base;
    private $delai;

    public function __construct($cleApi, $base = 'https://liseuse-pdf.com/api/gestion/v1', $delai = 10)
    {
        if (!preg_match('/^lsp_[A-Za-z0-9_-]{20,64}$/', (string) $cleApi)) {
            throw new InvalidArgumentException('Clé API Liseuse PDF invalide (lsp_…)');
        }
        $this->cleApi = $cleApi;
        $this->base = rtrim($base, '/');
        $this->delai = $delai;
    }

    private function appel($methode, $chemin, $corps = null, $idempotence = null)
    {
        $ch = curl_init($this->base . $chemin);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $methode,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->delai,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $this->cleApi,
                'Content-Type: application/json',
                'User-Agent: liseuse-pdf-php/1.1',
            ] + ($idempotence ? [3 => 'Idempotency-Key: ' . $idempotence] : []),
        ]);
        if ($corps !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($corps));
        }
        $reponse = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $erreur = curl_error($ch);
        curl_close($ch);
        if ($reponse === false) {
            throw new LiseusePDFException(0, 'reseau', 'Liseuse PDF injoignable : ' . $erreur);
        }
        $data = json_decode($reponse, true);
        if ($status < 200 || $status >= 300) {
            throw new LiseusePDFException($status, $data['error']['code'] ?? 'http_' . $status, $data['error']['message'] ?? 'Erreur ' . $status);
        }
        return $data;
    }

    /** Lien de lecture signé pour un abonné : ['url' => …, 'expire_le' => …, 'iframe' => …].
     *  $reference : votre identifiant d'abonné, pour révoquer ses liens plus tard. */
    public function creerLien($id, $jours = null, $page = null, $reference = null)
    {
        return $this->appel('POST', '/liens', array_filter(['id' => $id, 'jours' => $jours, 'page' => $page, 'reference' => $reference], function ($v) { return $v !== null; }));
    }

    /** Désactive tous les liens créés avec cette référence d'abonné. */
    public function revoquerLiens($reference)
    {
        return $this->appel('POST', '/liens/revoquer', ['reference' => $reference]);
    }

    /** Un numéro par son identifiant. */
    public function numero($id)
    {
        return $this->appel('GET', '/numeros?id=' . rawurlencode($id))['numero'];
    }

    /** Publie un PDF (chemin du fichier) : $champs = titre, publication, nouvelle_publication, parution, acces, lien_achat,
     *  telechargement, impression, domaine, adresse, remplace, couverture (chemin d'un JPEG). Permission « publication ». */
    public function publier($fichier, array $champs)
    {
        $pdf = file_get_contents($fichier);
        if ($pdf === false) {
            throw new InvalidArgumentException('Fichier illisible : ' . $fichier);
        }
        $domaine = $champs['domaine'] ?? null;
        $sha = $this->televerser($pdf, 'pdf', $domaine);
        if (!empty($champs['couverture'])) {
            $jpg = file_get_contents($champs['couverture']);
            $champs['couverture_sha256'] = $this->televerser($jpg, 'couverture', $domaine);
            $champs['couverture_taille'] = strlen($jpg);
        }
        unset($champs['couverture']);
        $corps = $champs + ['sha256' => $sha, 'taille' => strlen($pdf), 'nom_fichier' => basename($fichier)];
        return $this->appel('POST', '/numeros', $corps, 'publier-' . $sha . '-' . ($champs['remplace'] ?? ($champs['titre'] ?? '')));
    }

    private function televerser($octets, $type, $domaine)
    {
        $sha = hash('sha256', $octets);
        $t = $this->appel('POST', '/televersements', array_filter(['type' => $type, 'sha256' => $sha, 'taille' => strlen($octets), 'domaine' => $domaine]));
        if (empty($t['deja_present'])) {
            $ch = curl_init($t['url']);
            curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => $octets, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 600,
                CURLOPT_HTTPHEADER => ['Content-Type: ' . $t['en_tetes']['Content-Type']]]);
            curl_exec($ch);
            $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            if ($status < 200 || $status >= 300) {
                throw new LiseusePDFException($status, 'televersement', 'Envoi du fichier refusé par le stockage (' . $status . ')');
            }
        }
        return $sha;
    }

    /** Modifie un numéro : titre, acces, lien_achat, en_ligne, telechargement, impression, publication. */
    public function modifierNumero($id, array $champs)
    {
        return $this->appel('PATCH', '/numeros?id=' . rawurlencode($id), $champs)['numero'];
    }

    /** Vos numéros en ligne, du plus récent au plus ancien. */
    public function publications($publication = null, $limite = null)
    {
        $q = http_build_query(array_filter(['publication' => $publication, 'limite' => $limite]));
        return $this->appel('GET', '/publications' . ($q ? '?' . $q : ''))['publications'];
    }

    /** Lectures d'un numéro (30 derniers jours par défaut). */
    public function statistiques($id, $depuis = null)
    {
        return $this->appel('GET', '/statistiques?' . http_build_query(array_filter(['id' => $id, 'depuis' => $depuis])));
    }

    /** Formule, statut, limites et utilisation du compte. */
    public function compte()
    {
        return $this->appel('GET', '/compte')['compte'];
    }

    /** Vérifie l'en-tête Liseuse-Signature et renvoie l'événement décodé ; lève une exception sinon. */
    public static function verifierWebhook($corpsBrut, $entete, $secret, $tolerance = 300)
    {
        $t = '';
        $v1 = [];
        foreach (explode(',', (string) $entete) as $partie) {
            $kv = array_map('trim', explode('=', $partie, 2));
            if (count($kv) !== 2) {
                continue;
            }
            if ($kv[0] === 't') {
                $t = $kv[1];
            } elseif ($kv[0] === 'v1' && $kv[1] !== '') {
                $v1[] = $kv[1];
            }
        }
        if ($t === '' || !$v1) {
            throw new LiseusePDFException(400, 'signature_absente', 'En-tête Liseuse-Signature absent ou incomplet');
        }
        if (abs(time() - (int) $t) > $tolerance) {
            throw new LiseusePDFException(400, 'signature_expiree', 'Signature trop ancienne');
        }
        $attendu = hash_hmac('sha256', $t . '.' . $corpsBrut, $secret);
        foreach ($v1 as $signature) {
            if (hash_equals($attendu, $signature)) {
                return json_decode($corpsBrut, true);
            }
        }
        throw new LiseusePDFException(400, 'signature_invalide', 'Signature invalide');
    }
}
