<?php
// customer/includes/webpush.php
// ---------------------------------------------------------------------------
// Self-contained Web Push sender (VAPID + RFC 8291 "aes128gcm" payload
// encryption) using PHP's openssl + hash_hkdf. No external library required.
// ---------------------------------------------------------------------------

if (!function_exists('wp_b64url_encode')) {

    function wp_b64url_encode(string $bin): string {
        return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
    }
    function wp_b64url_decode(string $txt): string {
        $txt = strtr($txt, '-_', '+/');
        $pad = strlen($txt) % 4;
        if ($pad) $txt .= str_repeat('=', 4 - $pad);
        return base64_decode($txt) ?: '';
    }

    /** Build a PEM public key from a raw P-256 point (0x04||x||y, 65 bytes). */
    function wp_point_to_pem(string $point): string {
        $der = hex2bin('3059301306072a8648ce3d020106082a8648ce3d030107034200') . $point;
        return "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($der), 64, "\n") . "-----END PUBLIC KEY-----\n";
    }

    /** Convert a DER-encoded ECDSA signature to raw 64-byte (r||s) for ES256. */
    function wp_der_to_raw_sig(string $der): string {
        $off = 0; $len = strlen($der);
        if ($len < 8 || ord($der[$off++]) !== 0x30) return str_repeat("\x00", 64);
        $seq = ord($der[$off++]);
        if ($seq & 0x80) { $off += ($seq & 0x7f); }         // long-form seq length (rare for P-256)
        if (ord($der[$off++]) !== 0x02) return str_repeat("\x00", 64);
        $rlen = ord($der[$off++]); $r = substr($der, $off, $rlen); $off += $rlen;
        if (ord($der[$off++]) !== 0x02) return str_repeat("\x00", 64);
        $slen = ord($der[$off++]); $s = substr($der, $off, $slen);
        $r = str_pad(ltrim($r, "\x00"), 32, "\x00", STR_PAD_LEFT);
        $s = str_pad(ltrim($s, "\x00"), 32, "\x00", STR_PAD_LEFT);
        return substr($r, -32) . substr($s, -32);
    }

    /**
     * Send one encrypted Web Push message to a subscription.
     * $sub = ['endpoint'=>..., 'p256dh'=>b64url, 'auth'=>b64url]
     * Returns the HTTP status code (201 = accepted; 404/410 = gone).
     */
    function wp_send(array $sub, string $payload): int {
        require_once __DIR__ . '/../config/vapid.php';
        if (!function_exists('openssl_pkey_derive') || !function_exists('hash_hkdf')) return 0;

        $endpoint  = $sub['endpoint'];
        $ua_public = wp_b64url_decode($sub['p256dh']);  // 65 bytes
        $auth      = wp_b64url_decode($sub['auth']);    // 16 bytes
        if (strlen($ua_public) !== 65 || strlen($auth) < 16) return 0;

        // Ephemeral server keypair
        $eph = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
        if (!$eph) return 0;
        $d = openssl_pkey_get_details($eph);
        $as_public = "\x04"
            . str_pad($d['ec']['x'], 32, "\x00", STR_PAD_LEFT)
            . str_pad($d['ec']['y'], 32, "\x00", STR_PAD_LEFT);

        // ECDH shared secret with the subscriber's public key
        $uaKey = openssl_pkey_get_public(wp_point_to_pem($ua_public));
        if (!$uaKey) return 0;
        $shared = openssl_pkey_derive($uaKey, $eph);
        if (!$shared) return 0;

        // RFC 8291 key derivation
        $keyInfo = "WebPush: info\x00" . $ua_public . $as_public;
        $ikm     = hash_hkdf('sha256', $shared, 32, $keyInfo, $auth);

        // aes128gcm content encryption keys (RFC 8188)
        $salt  = random_bytes(16);
        $cek   = hash_hkdf('sha256', $ikm, 16, "Content-Encoding: aes128gcm\x00", $salt);
        $nonce = hash_hkdf('sha256', $ikm, 12, "Content-Encoding: nonce\x00", $salt);

        // Single record: plaintext + delimiter 0x02
        $tag = '';
        $ciphertext = openssl_encrypt($payload . "\x02", 'aes-128-gcm', $cek, OPENSSL_RAW_DATA, $nonce, $tag, '', 16);
        if ($ciphertext === false) return 0;
        $ciphertext .= $tag;

        // aes128gcm body: salt(16) || rs(4) || idlen(1) || keyid(as_public) || ciphertext
        $body = $salt . pack('N', 4096) . chr(65) . $as_public . $ciphertext;

        // VAPID JWT
        $u   = parse_url($endpoint);
        $aud = $u['scheme'] . '://' . $u['host'] . (isset($u['port']) ? ':' . $u['port'] : '');
        $h   = wp_b64url_encode(json_encode(['typ' => 'JWT', 'alg' => 'ES256']));
        $p   = wp_b64url_encode(json_encode(['aud' => $aud, 'exp' => time() + 12 * 3600, 'sub' => VAPID_SUBJECT]));
        $sig = '';
        openssl_sign($h . '.' . $p, $sig, VAPID_PRIVATE_PEM, OPENSSL_ALGO_SHA256);
        $jwt = $h . '.' . $p . '.' . wp_b64url_encode(wp_der_to_raw_sig($sig));

        $ch = curl_init($endpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT        => 8,
            CURLOPT_HTTPHEADER     => [
                'Authorization: vapid t=' . $jwt . ',k=' . VAPID_PUBLIC,
                'Content-Encoding: aes128gcm',
                'Content-Type: application/octet-stream',
                'TTL: 2419200',
            ],
        ]);
        curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return $status;
    }

    function dabil_push_ensure_table(mysqli $conn): void {
        @$conn->query(
            "CREATE TABLE IF NOT EXISTS `push_subscriptions` (
                `id` INT NOT NULL AUTO_INCREMENT,
                `user_id` INT NOT NULL,
                `endpoint` VARCHAR(512) NOT NULL,
                `p256dh` VARCHAR(255) NOT NULL,
                `auth` VARCHAR(255) NOT NULL,
                `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (`id`),
                UNIQUE KEY `uniq_ep` (`endpoint`),
                KEY `idx_user` (`user_id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"
        );
    }

    /** Push to every device a user has registered. Non-fatal; prunes dead subs. */
    function dabil_push_to_user(mysqli $conn, int $uid, string $title, string $body, string $url = 'dashboard.php'): void {
        dabil_push_ensure_table($conn);
        $payload = json_encode([
            'title' => $title,
            'body'  => $body,
            'url'   => $url,
            // Brand icon shown beside the notification popup (resolved
            // against the service worker's scope on the device).
            'icon'  => 'assets/img/noticon.png?v=2',
        ]);
        if (!($q = $conn->prepare("SELECT id, endpoint, p256dh, auth FROM push_subscriptions WHERE user_id=?"))) return;
        $q->bind_param("i", $uid);
        $q->execute();
        $rs = $q->get_result();
        while ($sub = $rs->fetch_assoc()) {
            try {
                $status = wp_send($sub, $payload);
                if ($status === 404 || $status === 410) {
                    @$conn->query("DELETE FROM push_subscriptions WHERE id=" . (int)$sub['id']);
                }
            } catch (\Throwable $e) { /* never break the caller */ }
        }
    }
}
