<?php
/**
 * Push notifications via Firebase Cloud Messaging — PHP edition.
 *
 * cPanel-friendly: uses only `curl` + `openssl` + `json` extensions
 * (all standard on shared PHP hosting). No Composer / no Python.
 *
 * Endpoints (registered in index.php):
 *   POST /api/devices/register-token       (any logged-in user)
 *   POST /api/admin/broadcast-notification (admin)
 *   POST /api/admin/notify-user            (admin, single recipient)
 *   GET  /api/admin/broadcasts             (admin, last N records)
 *   GET  /api/admin/devices                (admin, paginated tokens)
 *
 * The Firebase service-account JSON must live at:
 *   /api/secrets/firebase-service-account.json
 *
 * The path is settable via the FIREBASE_SERVICE_ACCOUNT env var if you
 * want to keep the file outside the document root.
 */

/* ───────────────────────────────────────────────────────────────
 *  ─ FCM access-token helper (OAuth 2.0 via JWT, RS256-signed) ─
 * ─────────────────────────────────────────────────────────────── */

function fcm_service_account_path(): string {
    $env = getenv('FIREBASE_SERVICE_ACCOUNT');
    if ($env && is_file($env)) return $env;
    return __DIR__ . '/../secrets/firebase-service-account.json';
}

function fcm_service_account(): array {
    static $cached = null;
    if ($cached !== null) return $cached;
    $p = fcm_service_account_path();
    if (!is_file($p)) {
        json_error(503, 'Firebase service account not found at ' . $p);
    }
    $raw = @file_get_contents($p);
    if ($raw === false) json_error(503, 'Cannot read Firebase service account');
    $cached = json_decode($raw, true);
    if (!$cached || empty($cached['project_id'])) {
        json_error(503, 'Firebase service account JSON is malformed');
    }
    return $cached;
}

/**
 * Mint (and cache) a fresh FCM OAuth 2.0 access token. Tokens last
 * an hour — we cache in APCu when available, otherwise on-disk so
 * shared hosts that disable APCu still avoid signing on every call.
 */
function fcm_access_token(): string {
    $sa = fcm_service_account();
    $cache_key = 'fcm_token_' . md5($sa['client_email']);
    $cache_file = sys_get_temp_dir() . '/' . $cache_key . '.json';

    // 1) APCu cache (fastest)
    if (function_exists('apcu_fetch')) {
        $cached = apcu_fetch($cache_key, $ok);
        if ($ok && is_array($cached) && $cached['exp'] > time() + 60) {
            return $cached['token'];
        }
    }
    // 2) File cache fallback
    if (is_file($cache_file)) {
        $raw = @file_get_contents($cache_file);
        $cached = $raw ? json_decode($raw, true) : null;
        if ($cached && ($cached['exp'] ?? 0) > time() + 60) {
            return $cached['token'];
        }
    }

    // ── Build + sign the JWT assertion ────────────────────────
    $now = time();
    $header = [ 'alg' => 'RS256', 'typ' => 'JWT' ];
    $claim  = [
        'iss'   => $sa['client_email'],
        'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
        'aud'   => 'https://oauth2.googleapis.com/token',
        'iat'   => $now,
        'exp'   => $now + 3600,
    ];
    $b64 = function ($a) { return rtrim(strtr(base64_encode(json_encode($a)), '+/', '-_'), '='); };
    $signing_input = $b64($header) . '.' . $b64($claim);

    $pk = openssl_pkey_get_private($sa['private_key']);
    if (!$pk) json_error(503, 'Bad Firebase private key');
    $sig = '';
    openssl_sign($signing_input, $sig, $pk, OPENSSL_ALGO_SHA256);
    if (PHP_VERSION_ID < 80000) openssl_free_key($pk);
    $assertion = $signing_input . '.' . rtrim(strtr(base64_encode($sig), '+/', '-_'), '=');

    // ── Exchange the JWT for an access token ──────────────────
    $ch = curl_init('https://oauth2.googleapis.com/token');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query([
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion'  => $assertion,
        ]),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
    ]);
    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $err  = curl_error($ch);
    curl_close($ch);
    if ($resp === false || $code !== 200) {
        json_error(502, 'FCM token exchange failed: HTTP ' . $code . ' ' . $err . ' ' . substr((string)$resp, 0, 200));
    }
    $j = json_decode($resp, true);
    if (empty($j['access_token'])) json_error(502, 'FCM token exchange returned no access_token');

    $entry = [ 'token' => $j['access_token'], 'exp' => time() + (int)($j['expires_in'] ?? 3600) ];
    if (function_exists('apcu_store')) apcu_store($cache_key, $entry, 3500);
    @file_put_contents($cache_file, json_encode($entry));
    @chmod($cache_file, 0600);
    return $entry['token'];
}

/**
 * Send ONE FCM message via HTTP v1.
 * Returns ['ok' => bool, 'status' => int, 'error' => ?string].
 */
function fcm_send_one(array $message, string $token, string $project_id): array {
    $url = "https://fcm.googleapis.com/v1/projects/{$project_id}/messages:send";
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode(['message' => $message]),
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json; charset=UTF-8',
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
    ]);
    $resp = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($resp === false) return ['ok' => false, 'status' => 0, 'error' => 'curl failed'];
    if ($code === 200) return ['ok' => true, 'status' => 200, 'error' => null];
    $j = json_decode((string)$resp, true);
    $err = $j['error']['status'] ?? ('HTTP ' . $code);
    return ['ok' => false, 'status' => $code, 'error' => $err];
}

/**
 * Build the `message` object Firebase expects.
 *
 *   $opts:
 *     title*            string
 *     body*             string
 *     route             string  (e.g. '/live-tv') — added to data
 *     image_url         string  (rich notification image)
 *     action_label      string  (text for the "Watch Now" button)
 *     data              array<string,scalar>
 *
 * Token is set by the caller (per-recipient).
 */
function fcm_build_message(array $opts): array {
    $title = trim((string)($opts['title'] ?? ''));
    $body  = trim((string)($opts['body']  ?? ''));
    if ($title === '' || $body === '') json_error(400, 'title and body are required');

    $data = [];
    foreach (($opts['data'] ?? []) as $k => $v) $data[(string)$k] = (string)$v;
    if (!empty($opts['route']))        $data['route'] = (string)$opts['route'];
    if (!empty($opts['action_label'])) $data['action_label'] = (string)$opts['action_label'];
    $data['fromNotification'] = '1';

    $notification = [ 'title' => $title, 'body' => $body ];
    if (!empty($opts['image_url'])) $notification['image'] = (string)$opts['image_url'];

    // Android-specific tweaks: high priority + custom channel + image
    $android_notification = [
        'channel_id' => 'default_notification_channel',
        'priority'   => 'HIGH',
        'click_action' => 'FLUTTER_NOTIFICATION_CLICK',
    ];
    if (!empty($opts['image_url'])) $android_notification['image'] = (string)$opts['image_url'];

    return [
        'notification' => $notification,
        'data'         => $data,
        'android'      => [
            'priority'     => 'HIGH',
            'notification' => $android_notification,
        ],
    ];
}

/* ───────────────────────────────────────────────────────────────
 *  ─ DB collections (Mongo via lib/db.php → get_db) ─────────────
 * ─────────────────────────────────────────────────────────────── */

function fcm_tokens_col() { return get_db()->fcm_tokens; }
function fcm_broadcasts_col() { return get_db()->fcm_broadcasts; }

/* ───────────────────────────────────────────────────────────────
 *  ─ Endpoint handlers ─────────────────────────────────────────
 * ─────────────────────────────────────────────────────────────── */

/** POST /api/devices/register-token */
function route_devices_register_token(): void {
    $u = require_auth();
    $b = read_json_body();
    $token = trim((string)($b['token'] ?? ''));
    $device_id = trim((string)($b['device_id'] ?? ''));
    if (strlen($token) < 10 || $device_id === '') json_error(400, 'token and device_id are required');

    $now = gmdate('c');
    $col = fcm_tokens_col();
    $col->updateOne(
        ['device_id' => $device_id],
        [
            '$set' => [
                'token'       => $token,
                'device_id'   => $device_id,
                'platform'    => $b['platform']    ?? 'android',
                'app_version' => $b['app_version'] ?? null,
                'user_id'     => $u['id'],
                'is_active'   => true,
                'updated_at'  => $now,
            ],
            '$setOnInsert' => [ 'created_at' => $now ],
        ],
        ['upsert' => true]
    );
    send_json([ 'registered' => true, 'device_id' => $device_id ]);
}

/** Internal: send to a list of token-rows, log + prune stale, return stats. */
function fcm_send_batch(array $rows, array $opts, string $kind /* 'broadcast' | 'single' */): array {
    if (empty($rows)) {
        return ['queued' => false, 'total_tokens' => 0, 'sent' => 0, 'failed' => 0, 'pruned' => 0,
                'error' => 'No registered devices'];
    }
    $sa = fcm_service_account();
    $access = fcm_access_token();
    $message_proto = fcm_build_message($opts);

    $sent = 0; $failed = 0; $pruned = 0; $stale_ids = [];
    foreach ($rows as $r) {
        $msg = $message_proto;
        $msg['token'] = $r['token'];
        $res = fcm_send_one($msg, $access, $sa['project_id']);
        if ($res['ok']) { $sent++; continue; }
        $failed++;
        $err = strtoupper((string)$res['error']);
        if (in_array($err, ['NOT_FOUND', 'UNREGISTERED', 'INVALID_ARGUMENT'], true)
            || str_contains($err, 'NOT-REGISTERED')) {
            $stale_ids[] = $r['device_id'];
        }
    }
    if (!empty($stale_ids)) {
        fcm_tokens_col()->updateMany(
            ['device_id' => ['$in' => $stale_ids]],
            ['$set' => ['is_active' => false, 'deactivated_at' => gmdate('c')]]
        );
        $pruned = count($stale_ids);
    }

    $log = [
        'kind'         => $kind,
        'title'        => $opts['title'],
        'body'         => $opts['body'],
        'route'        => $opts['route']     ?? null,
        'image_url'    => $opts['image_url'] ?? null,
        'action_label' => $opts['action_label'] ?? null,
        'total_tokens' => count($rows),
        'sent'         => $sent,
        'failed'       => $failed,
        'pruned'       => $pruned,
        'created_at'   => gmdate('c'),
    ];
    if (!empty($opts['target_user_id'])) $log['target_user_id'] = $opts['target_user_id'];
    fcm_broadcasts_col()->insertOne($log);

    return ['queued' => true, 'total_tokens' => count($rows),
            'sent' => $sent, 'failed' => $failed, 'pruned' => $pruned, 'error' => null];
}

/** POST /api/admin/broadcast-notification */
function route_admin_broadcast(): void {
    require_admin();
    $b = read_json_body();
    $rows = iterator_to_array(fcm_tokens_col()->find(
        ['is_active' => true],
        ['projection' => ['_id' => 0, 'token' => 1, 'device_id' => 1]]
    ));
    $rows = array_map(function ($d) { return is_array($d) ? $d : (array)$d; }, $rows);

    $opts = [
        'title'        => (string)($b['title']        ?? ''),
        'body'         => (string)($b['body']         ?? ''),
        'route'        =>          ($b['route']        ?? null),
        'image_url'    =>          ($b['image_url']    ?? null),
        'action_label' =>          ($b['action_label'] ?? null),
        'data'         =>          ($b['data']         ?? []),
    ];
    send_json(fcm_send_batch($rows, $opts, 'broadcast'));
}

/** POST /api/admin/notify-user */
function route_admin_notify_user(): void {
    require_admin();
    $b = read_json_body();
    $user_id = trim((string)($b['user_id'] ?? ''));
    if ($user_id === '') json_error(400, 'user_id is required');

    $rows = iterator_to_array(fcm_tokens_col()->find(
        ['user_id' => $user_id, 'is_active' => true],
        ['projection' => ['_id' => 0, 'token' => 1, 'device_id' => 1]]
    ));
    $rows = array_map(function ($d) { return is_array($d) ? $d : (array)$d; }, $rows);

    $opts = [
        'title'          => (string)($b['title']        ?? ''),
        'body'           => (string)($b['body']         ?? ''),
        'route'          =>          ($b['route']        ?? null),
        'image_url'      =>          ($b['image_url']    ?? null),
        'action_label'   =>          ($b['action_label'] ?? null),
        'data'           =>          ($b['data']         ?? []),
        'target_user_id' => $user_id,
    ];
    send_json(fcm_send_batch($rows, $opts, 'single'));
}

/** GET /api/admin/broadcasts?limit=20 */
function route_admin_broadcasts(): void {
    require_admin();
    $limit = max(1, min(200, (int)($_GET['limit'] ?? 20)));
    $cursor = fcm_broadcasts_col()->find(
        [],
        [
            'projection' => ['_id' => 0],
            'sort'       => ['created_at' => -1],
            'limit'      => $limit,
        ]
    );
    $out = [];
    foreach ($cursor as $d) $out[] = is_array($d) ? $d : (array)$d;
    send_json(['broadcasts' => $out]);
}

/** GET /api/admin/devices?limit=50 — paginated tokens for the admin */
function route_admin_devices(): void {
    require_admin();
    $limit = max(1, min(200, (int)($_GET['limit'] ?? 50)));
    $skip  = max(0, (int)($_GET['skip']  ?? 0));
    $col   = fcm_tokens_col();
    $cursor = $col->find(
        [],
        [
            'projection' => ['_id' => 0, 'token' => 0],
            'sort'       => ['updated_at' => -1],
            'skip'       => $skip,
            'limit'      => $limit,
        ]
    );
    $out = [];
    foreach ($cursor as $d) $out[] = is_array($d) ? $d : (array)$d;
    $total = $col->countDocuments([]);
    $active = $col->countDocuments(['is_active' => true]);
    send_json([ 'devices' => $out, 'total' => $total, 'active' => $active ]);
}
