<?php
// ─────────────────────────────────────────────────────────────
//  lib/QrPass.php — QR membership pass: sign, verify, render
// ─────────────────────────────────────────────────────────────
//  Tokens are stateless: base64url(JSON payload) + '.' + base64url(HMAC-SHA256).
//  No database lookup needed to verify a token — just a signature check.
//  Tokens expire after MAX_AGE_SECONDS (default 24 h). The customer
//  dashboard regenerates the token on every page load, so there is
//  always a fresh code available.
// ─────────────────────────────────────────────────────────────
if (!defined('DB_HOST')) require_once dirname(__DIR__) . '/lib.php';

class QrPass {

    const MAX_AGE_SECONDS = 86400; // 24 hours

    // ── Signing key ───────────────────────────────────────────

    private static function key(): string {
        if (defined('APP_KEY') && APP_KEY !== 'change-this-to-a-32-char-random-secret-key') {
            return APP_KEY;
        }
        // Fallback: derive from DB credentials so it's at least unique per install.
        // This warns loudly in the error log so the admin knows to set APP_KEY.
        error_log('QrPass: APP_KEY is not set. Set a strong APP_KEY in config.php.');
        return hash('sha256', (defined('DB_PASS') ? DB_PASS : '') . (defined('DB_NAME') ? DB_NAME : ''));
    }

    // ── Encoding ──────────────────────────────────────────────

    /**
     * Build a signed token string for embedding in a QR code.
     * Format: <base64url payload>.<base64url HMAC>
     */
    public static function encode(int $membershipId, int $customerId): string {
        $payload = json_encode([
            'mid' => $membershipId,
            'cid' => $customerId,
            'ts'  => time(),
        ]);
        $b64 = self::b64url($payload);
        $sig = self::b64url(hash_hmac('sha256', $b64, self::key(), true));
        return $b64 . '.' . $sig;
    }

    // ── Decoding & verification ───────────────────────────────

    /**
     * Verify and decode a QR pass token.
     * Returns the decoded payload array on success.
     * Throws RuntimeException on invalid signature, bad format, or expiry.
     */
    public static function decode(string $token, int $maxAge = self::MAX_AGE_SECONDS): array {
        $parts = explode('.', $token, 2);
        if (count($parts) !== 2) {
            throw new \RuntimeException('Invalid QR token format');
        }
        [$b64, $sig] = $parts;

        // Constant-time signature check
        $expected = self::b64url(hash_hmac('sha256', $b64, self::key(), true));
        if (!hash_equals($expected, $sig)) {
            throw new \RuntimeException('QR token signature invalid');
        }

        $raw  = base64_decode(strtr($b64, '-_', '+/') . str_repeat('=', 3 - (strlen($b64) + 3) % 4 % 4));
        $data = json_decode($raw, true);
        if (!is_array($data) || !isset($data['mid'], $data['cid'], $data['ts'])) {
            throw new \RuntimeException('Malformed QR payload');
        }

        if (time() - (int)$data['ts'] > $maxAge) {
            throw new \RuntimeException('QR token expired — please refresh your dashboard');
        }

        return [
            'membership_id' => (int) $data['mid'],
            'customer_id'   => (int) $data['cid'],
            'issued_at'     => (int) $data['ts'],
        ];
    }

    // ── PNG generation ────────────────────────────────────────

    /**
     * Returns a QR code as a base64 data URI (data:image/png;base64,...).
     *
     * Tries three renderers in order:
     *   1. endroid/qr-code (Composer) — best quality
     *   2. phpqrcode (vendor/phpqrcode/qrlib.php) — common on shared hosts
     *   3. Pure-PHP fallback using a Google Charts-compatible SVG raster
     *
     * The PNG is generated fresh on every call; do NOT cache across requests
     * (the token timestamp changes every page load).
     */
    public static function png(string $token, int $size = 200): string {
        // ── Renderer 1: endroid/qr-code ───────────────────────
        if (class_exists('\\Endroid\\QrCode\\QrCode')) {
            $qr     = \Endroid\QrCode\QrCode::create($token)
                ->setSize($size)
                ->setMargin(10);
            $writer = new \Endroid\QrCode\Writer\PngWriter();
            $result = $writer->write($qr);
            return 'data:image/png;base64,' . base64_encode($result->getString());
        }

        // ── Renderer 2: phpqrcode ─────────────────────────────
        $phpqr = dirname(__DIR__) . '/vendor/phpqrcode/qrlib.php';
        if (file_exists($phpqr)) {
            require_once $phpqr;
            ob_start();
            \QRcode::png($token, false, QR_ECLEVEL_M, (int)ceil($size / 41), 2);
            $raw = ob_get_clean();
            if ($raw) {
                return 'data:image/png;base64,' . base64_encode($raw);
            }
        }

        // ── Renderer 3: goqr.me API fallback ─────────────────
        // Only used when neither library is present.
        // Safe for dev; for production, install one of the above.
        $url = 'https://api.qrserver.com/v1/create-qr-code/?size='
             . $size . 'x' . $size
             . '&data=' . rawurlencode($token);
        $raw = @file_get_contents($url);
        if ($raw) {
            return 'data:image/png;base64,' . base64_encode($raw);
        }

        // ── Last resort: return an SVG placeholder ────────────
        $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' . $size . '" height="' . $size . '">'
             . '<rect width="100%" height="100%" fill="#f3f4f6"/>'
             . '<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" '
             . 'font-family="monospace" font-size="11" fill="#6b7280">QR unavailable</text>'
             . '</svg>';
        return 'data:image/svg+xml;base64,' . base64_encode($svg);
    }

    // ── Helpers ───────────────────────────────────────────────

    private static function b64url(string $data): string {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
}
