<?php
/**
 * Fichier  : api/push.php
 * Auteur   : KREMER Régis
 * Version  : 2.5.0
 * Date     : 2026-03-01
 * Description : API REST — Notifications Web Push avec signature VAPID ES256
 *
 * Changelog :
 *   v2.2.0 — Version initiale avec signature VAPID stub
 *   v2.5.0 — Signature JWT ES256 complète via OpenSSL, envoi push fonctionnel
 */
require_once dirname(__DIR__).'/config.php';
startSession();

$method = $_SERVER['REQUEST_METHOD'];
$body   = json_decode(file_get_contents('php://input'), true) ?? [];

// ════════════════════════════════════════════════════════════════
// VAPID KEYS
// ════════════════════════════════════════════════════════════════
function getVapidKeys(): array {
    $s = getSettings();
    if (!empty($s['vapidPublicKey']) && !empty($s['vapidPrivateKey'])) {
        return ['public' => $s['vapidPublicKey'], 'private' => $s['vapidPrivateKey']];
    }
    $keys = generateVapidKeys();
    $s['vapidPublicKey']  = $keys['public'];
    $s['vapidPrivateKey'] = $keys['private'];
    saveSettings($s);
    return $keys;
}

function generateVapidKeys(): array {
    $key = openssl_pkey_new(['curve_name' => 'prime256v1', 'private_key_type' => OPENSSL_KEYTYPE_EC]);
    if (!$key) return ['public' => '', 'private' => ''];
    $det     = openssl_pkey_get_details($key);
    $pubKey  = rtrim(strtr(base64_encode(chr(4) . $det['ec']['x'] . $det['ec']['y']), '+/', '-_'), '=');
    $privKey = rtrim(strtr(base64_encode($det['ec']['d']), '+/', '-_'), '=');
    return ['public' => $pubKey, 'private' => $privKey];
}

// ════════════════════════════════════════════════════════════════
// JWT ES256 — signature ECDSA P-256 complète
// ════════════════════════════════════════════════════════════════
function b64url(string $data): string {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function b64urlDecode(string $data): string {
    $pad  = strlen($data) % 4;
    if ($pad) $data .= str_repeat('=', 4 - $pad);
    return base64_decode(strtr($data, '-_', '+/'));
}

/**
 * Reconstructs an EC PEM private key from a raw base64url-encoded scalar d.
 */
function vapidPrivateToPem(string $privBase64url): string {
    $d = b64urlDecode($privBase64url);
    $d = str_pad($d, 32, "\x00", STR_PAD_LEFT); // pad to 32 bytes

    // OID P-256 : 1.2.840.10045.3.1.7
    $oidP256     = "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07";
    // [0] EXPLICIT namedCurve
    $namedCurve  = "\xa0" . chr(strlen($oidP256)) . $oidP256;
    // SEC1 ECPrivateKey : SEQUENCE { INTEGER 1, OCTET STRING d, [0] namedCurve }
    $inner       = "\x02\x01\x01"                           // INTEGER 1
                 . "\x04" . chr(strlen($d)) . $d             // OCTET STRING d
                 . $namedCurve;
    $der         = "\x30" . chr(strlen($inner)) . $inner;

    return "-----BEGIN EC PRIVATE KEY-----\n"
        . chunk_split(base64_encode($der), 64, "\n")
        . "-----END EC PRIVATE KEY-----\n";
}

function createVapidJwt(string $endpoint, array $vapid): ?string {
    if (empty($vapid['private'])) return null;

    $parsed   = parse_url($endpoint);
    $audience = $parsed['scheme'] . '://' . $parsed['host'];

    $header  = b64url(json_encode(['alg' => 'ES256', 'typ' => 'JWT']));
    $payload = b64url(json_encode([
        'aud' => $audience,
        'exp' => time() + 43200,
        'sub' => 'mailto:admin@stockmaster.local',
    ]));
    $sigInput = $header . '.' . $payload;

    // Signer avec la clé privée ECDSA P-256
    $pem = vapidPrivateToPem($vapid['private']);
    $key = openssl_pkey_get_private($pem);
    if (!$key) return null;

    $signature = '';
    if (!openssl_sign($sigInput, $signature, $key, OPENSSL_ALGO_SHA256)) return null;

    // La signature openssl est en DER — convertir en raw (r||s, 64 bytes)
    // Format DER: 30 len 02 rlen r 02 slen s
    $pos = 2; // skip 30 + total length byte(s)
    if (ord($signature[$pos]) === 0x02) {
        $pos++; $rLen = ord($signature[$pos++]);
        $r = substr($signature, $pos, $rLen); $pos += $rLen;
        $pos++; $sLen = ord($signature[$pos++]);
        $s = substr($signature, $pos, $sLen);
        // Remove leading zero padding, pad to 32 bytes
        $r = str_pad(ltrim($r, "\x00"), 32, "\x00", STR_PAD_LEFT);
        $s = str_pad(ltrim($s, "\x00"), 32, "\x00", STR_PAD_LEFT);
        $rawSig = $r . $s;
    } else {
        // Fallback: use signature as-is
        $rawSig = $signature;
    }

    return $header . '.' . $payload . '.' . b64url($rawSig);
}

// ════════════════════════════════════════════════════════════════
// SUBSCRIPTIONS
// ════════════════════════════════════════════════════════════════
function loadSubscriptions(): array { return loadData('push_subscriptions'); }
function saveSubscriptions(array $s): void { saveData('push_subscriptions', $s); }

function addSubscription(array $sub, int $uid): array {
    $subs = loadSubscriptions();
    $ep   = $sub['endpoint'] ?? '';
    foreach ($subs as &$s) {
        if (($s['endpoint'] ?? '') === $ep) {
            $s['userId'] = $uid; $s['keys'] = $sub['keys'] ?? []; $s['updatedAt'] = date('c');
            saveSubscriptions($subs); return $s;
        }
    }
    $new = ['id'=>genId(),'userId'=>$uid,'endpoint'=>$ep,'keys'=>$sub['keys']??[],'createdAt'=>date('c'),'updatedAt'=>date('c')];
    $subs[] = $new; saveSubscriptions($subs);
    return $new;
}

function removeSubscription(string $ep): bool {
    $subs = loadSubscriptions();
    $f    = array_values(array_filter($subs, fn($s) => ($s['endpoint']??'') !== $ep));
    if (count($f) !== count($subs)) { saveSubscriptions($f); return true; }
    return false;
}

// ════════════════════════════════════════════════════════════════
// ENVOI PUSH (Web Push sans chiffrement body — notification visible)
// Pour un vrai chiffrement du payload, il faudrait implementer
// RFC 8291 (aes128gcm). Ici on envoie la subscription de facade
// et le SW affiche la notif depuis les données du service worker.
// ════════════════════════════════════════════════════════════════
function sendPushNotification(array $sub, array $payload, ?string &$debugInfo = null): bool {
    $ep   = $sub['endpoint'] ?? '';
    $keys = $sub['keys']     ?? [];
    if (!$ep) { $debugInfo = 'no_endpoint'; return false; }

    $vapid = getVapidKeys();
    $jwt   = createVapidJwt($ep, $vapid);
    if (!$jwt) { $debugInfo = 'jwt_failed'; return false; }

    // Envoi sans payload chiffré (compatible RFC 8030 sans RFC 8291)
    // Le SW utilise ses valeurs par défaut quand event.data est absent
    $headers = [
        'TTL: 86400',
        'Urgency: normal',
        'Authorization: vapid t=' . $jwt . ', k=' . $vapid['public'],
        'Content-Length: 0',
    ];

    $ch = curl_init($ep);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => '',      // body vide — pas de chiffrement requis
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $response = curl_exec($ch);
    $code     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err      = curl_error($ch);
    curl_close($ch);

    $debugInfo = "http=$code" . ($err ? " curl=$err" : '') . ($response ? " resp=".substr($response,0,60) : '');
    error_log("[Push] endpoint=".substr($ep,0,50)." $debugInfo");

    if ($code === 410 || $code === 404) removeSubscription($ep);
    return $code >= 200 && $code < 300;
}

function notifyAdmins(array $payload, ?int $excludeUserId = null): int {
    $subs    = loadSubscriptions();
    $users   = loadData('users');
    $adminIds = [];
    foreach ($users as $u) {
        if (($u['role']??'') === 'admin' && ($u['active']??true)) $adminIds[] = (int)$u['id'];
    }
    $sent = 0;
    foreach ($subs as $sub) {
        $uid = (int)($sub['userId'] ?? 0);
        if ($uid === $excludeUserId) continue;
        if (!in_array($uid, $adminIds)) continue;
        if (sendPushNotification($sub, $payload)) $sent++;
    }
    return $sent;
}

// ════════════════════════════════════════════════════════════════
// ROUTES
// ════════════════════════════════════════════════════════════════
switch ($method) {
    case 'GET':
        requireAuth();
        $vapid = getVapidKeys();
        jsonResponse(['publicKey' => $vapid['public']]);
        break;

    case 'POST':
        csrfCheck();
        $action = $body['action'] ?? '';

        if ($action === 'subscribe') {
            requireAuth();
            $user = currentUser();
            $sub  = $body['subscription'] ?? [];
            if (empty($sub['endpoint'])) jsonError('Endpoint manquant');
            jsonResponse(['success' => true, 'subscription' => addSubscription($sub, (int)$user['id'])]);
        }

        if ($action === 'unsubscribe') {
            requireAuth();
            $ep = $body['endpoint'] ?? '';
            if (!$ep) jsonError('Endpoint manquant');
            jsonResponse(['success' => removeSubscription($ep)]);
        }

        if ($action === 'test') {
            requireAdmin();
            $payload = [
                'title' => '✅ Test StockMaster Pro',
                'body'  => 'Les notifications push fonctionnent correctement !',
                'type'  => 'info',
                'icon'  => '/sm/assets/img/icon-192.png',
                'data'  => ['url' => '/sm/'],
            ];
            // Envoyer à tous les abonnements de cet appareil (endpoint actuel si possible)
            // ou à tous les admins abonnés — pour le test on envoie à tout le monde
            $subs = loadSubscriptions();
            $sent = 0;
            $errors = [];
            foreach ($subs as $sub) {
                $debugInfo = null;
                $result = sendPushNotification($sub, $payload, $debugInfo);
                if ($result) {
                    $sent++;
                } else {
                    $errors[] = ['endpoint' => substr($sub['endpoint'] ?? '', 0, 50), 'debug' => $debugInfo];
                }
            }
            jsonResponse(['success' => true, 'sent' => $sent, 'total' => count($subs), 'errors' => $errors]);
        }

        jsonError('Action inconnue');

    default:
        jsonError('Méthode non supportée', 405);
}
