<?php
/**
 * Ragreen - Firebase Cloud Messaging (push notifications)
 *
 * Sends pushes via Firebase's HTTP v1 API. Hand-rolled instead of pulling
 * in Google's PHP client library via Composer - same "stay dependency-free"
 * choice already made for JWT auth (see includes/jwt.php). The pieces:
 *
 *   1. Build a signed JWT asserting who we are (the service account).
 *   2. Exchange that JWT for a short-lived OAuth2 access token.
 *   3. Use the access token as a Bearer header on the actual push call.
 *
 * Step 2's result is cached to a file for its ~1 hour lifetime, so a batch
 * of pushes (e.g. notifying several sellers from one checkout) doesn't
 * re-authenticate per push.
 *
 * SILENT NO-OP BY DESIGN: if FCM isn't configured yet (no service account
 * file at FCM_SERVICE_ACCOUNT_PATH), every function here just returns
 * immediately. Push notifications are an enhancement - checkout, order
 * fulfillment, admin actions, etc. must keep working perfectly whether or
 * not FCM is set up, and must never fail or slow down because Firebase is
 * unreachable. See create_notification() in notifications.php, the single
 * call site every part of the app already goes through.
 */

function fcm_is_configured(): bool
{
    return defined('FCM_SERVICE_ACCOUNT_PATH')
        && FCM_SERVICE_ACCOUNT_PATH !== ''
        && is_readable(FCM_SERVICE_ACCOUNT_PATH);
}

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

/**
 * Returns a valid OAuth2 access token for the Firebase service account, or
 * null if FCM isn't configured or the token exchange fails. Caches to a
 * temp file so repeated calls within the token's lifetime are free.
 */
function fcm_get_access_token(): ?string
{
    if (!fcm_is_configured()) {
        return null;
    }

    $cachePath = sys_get_temp_dir() . '/ragreen_fcm_token.json';
    if (is_readable($cachePath)) {
        $cached = json_decode((string) file_get_contents($cachePath), true);
        if (is_array($cached) && ($cached['expires_at'] ?? 0) > time() + 60) {
            return $cached['access_token'];
        }
    }

    $serviceAccount = json_decode((string) file_get_contents(FCM_SERVICE_ACCOUNT_PATH), true);
    if (!is_array($serviceAccount) || empty($serviceAccount['private_key']) || empty($serviceAccount['client_email'])) {
        error_log('FCM: service account file is missing private_key or client_email.');
        return null;
    }

    $now = time();
    $header = ['alg' => 'RS256', 'typ' => 'JWT'];
    $claims = [
        'iss'   => $serviceAccount['client_email'],
        'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
        'aud'   => 'https://oauth2.googleapis.com/token',
        'iat'   => $now,
        'exp'   => $now + 3600,
    ];
    $signingInput = fcm_base64url_encode(json_encode($header)) . '.' . fcm_base64url_encode(json_encode($claims));

    $signature = '';
    $signed = openssl_sign($signingInput, $signature, $serviceAccount['private_key'], OPENSSL_ALGO_SHA256);
    if (!$signed) {
        error_log('FCM: failed to sign JWT with service account private key.');
        return null;
    }
    $assertion = $signingInput . '.' . fcm_base64url_encode($signature);

    $ch = curl_init('https://oauth2.googleapis.com/token');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_POSTFIELDS => http_build_query([
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion'  => $assertion,
        ]),
    ]);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $body = json_decode((string) $response, true);
    if ($httpCode !== 200 || empty($body['access_token'])) {
        error_log('FCM: OAuth2 token exchange failed - ' . $response);
        return null;
    }

    @file_put_contents($cachePath, json_encode([
        'access_token' => $body['access_token'],
        'expires_at'   => $now + (int) ($body['expires_in'] ?? 3600),
    ]));

    return $body['access_token'];
}

/**
 * Sends one push to one device token. Returns true on success. On a
 * definitive "this token is dead" response from FCM (unregistered /
 * invalid), removes it from device_tokens so it stops being tried -
 * self-cleaning, so uninstalled apps and expired tokens don't accumulate
 * forever.
 */
function fcm_send_to_token(PDO $pdo, string $projectId, string $accessToken, string $deviceToken, string $title, string $body, array $data = []): bool
{
    // FCM requires all `data` values to be strings.
    $stringData = array_map('strval', $data);

    $payload = [
        'message' => [
            'token' => $deviceToken,
            'notification' => ['title' => $title, 'body' => $body],
            'data' => $stringData,
        ],
    ];

    $ch = curl_init("https://fcm.googleapis.com/v1/projects/$projectId/messages:send");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $accessToken,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 200) {
        return true;
    }

    $body2 = json_decode((string) $response, true);
    $status = $body2['error']['status'] ?? '';
    if (in_array($status, ['UNREGISTERED', 'INVALID_ARGUMENT', 'NOT_FOUND'], true)) {
        $pdo->prepare('DELETE FROM device_tokens WHERE token = ?')->execute([$deviceToken]);
    } else {
        error_log("FCM: push failed ($httpCode, $status) - $response");
    }
    return false;
}

/**
 * Pushes to every device registered for a user. This is the function
 * create_notification() calls - nothing else in the codebase should call
 * fcm_send_to_token() directly.
 */
function fcm_notify_user(PDO $pdo, int $userId, string $title, string $body, array $data = []): void
{
    if (!fcm_is_configured()) {
        return;
    }
    try {
        $accessToken = fcm_get_access_token();
        if (!$accessToken) {
            return;
        }
        $stmt = $pdo->prepare('SELECT token FROM device_tokens WHERE user_id = ?');
        $stmt->execute([$userId]);
        $tokens = $stmt->fetchAll(PDO::FETCH_COLUMN);
        foreach ($tokens as $token) {
            fcm_send_to_token($pdo, FCM_PROJECT_ID, $accessToken, $token, $title, $body, $data);
        }
    } catch (Throwable $e) {
        // Never let a push failure break the feature that triggered it.
        error_log('FCM: unexpected error in fcm_notify_user - ' . $e->getMessage());
    }
}
