<?php
declare(strict_types=1);

namespace Core;

/**
 * Thin wrapper around Expo's push notification service. Reads device tokens
 * from `push_tokens` and POSTs to https://exp.host/--/api/v2/push/send.
 *
 * Fire-and-forget — we never block the request that triggered the push.
 * Errors are logged to error_log but never thrown to the caller; a
 * notification that fails should not roll back a successful business action.
 */
final class Push
{
    private const EXPO_URL = 'https://exp.host/--/api/v2/push/send';

    /**
     * Send a notification to every device the given user has registered.
     * @param array{title:string,body:string,data?:array} $msg
     */
    public static function toUser(int $userId, array $msg): void
    {
        $tokens = array_column(
            DB::all('SELECT expo_token FROM push_tokens WHERE user_id = :u', ['u' => $userId]),
            'expo_token'
        );
        if (!$tokens) return;
        self::sendBatch($tokens, $msg);
    }

    /**
     * Broadcast to every user holding the given role code (e.g. 'NocDesk').
     * Used for team-wide alerts where any operator on shift should see the
     * notification — e.g. "FT extended ETA, please update customer comms."
     *
     * $excludeUserId silences the originator so a NOC operator triggering an
     * action doesn't get their own push echoed back.
     *
     * @param array{title:string,body:string,data?:array} $msg
     */
    public static function toRole(string $roleCode, array $msg, ?int $excludeUserId = null): void
    {
        $sql = "SELECT p.expo_token
                  FROM push_tokens p
                  JOIN users u ON u.user_id = p.user_id
                  JOIN roles r ON r.role_id = u.role_id
                 WHERE r.code = :rc AND u.deleted = 0";
        $params = ['rc' => $roleCode];
        if ($excludeUserId !== null) {
            $sql .= ' AND p.user_id <> :ex';
            $params['ex'] = $excludeUserId;
        }
        $tokens = array_column(DB::all($sql, $params), 'expo_token');
        if (!$tokens) return;
        self::sendBatch($tokens, $msg);
    }

    /** @param string[] $tokens */
    public static function sendBatch(array $tokens, array $msg): void
    {
        $tokens = array_values(array_filter(array_unique($tokens)));
        if (!$tokens) return;

        $payload = array_map(fn($t) => [
            'to'       => $t,
            'title'    => (string)($msg['title'] ?? 'SiteTracker'),
            'body'     => (string)($msg['body']  ?? ''),
            'data'     => $msg['data'] ?? null,
            'sound'    => 'default',
            'priority' => 'high',
        ], $tokens);

        $ch = curl_init(self::EXPO_URL);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_HTTPHEADER     => [
                'Accept: application/json',
                'Accept-encoding: gzip, deflate',
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS     => json_encode(count($payload) === 1 ? $payload[0] : $payload),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 5,    // never block a request > 5s on push
            CURLOPT_CONNECTTIMEOUT => 2,
        ]);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($err || $code >= 400) {
            error_log("[push] failed (HTTP {$code}): {$err} | resp=" . substr((string)$resp, 0, 200));
        }
    }
}
