<?php
/**
 * Push notifications via Firebase Cloud Messaging — PHP/MySQL edition.
 *
 * cPanel-friendly: uses PHP's built-in `curl` + `openssl` + PDO/MySQL
 * (the same DB used by users / settings tables — no Mongo, no Composer).
 *
 * Tables (auto-created on first request):
 *   • fcm_tokens      — one row per device install
 *   • fcm_broadcasts  — audit log of every push the admin sent
 *
 * 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
 * (Settable via the FIREBASE_SERVICE_ACCOUNT env var.)
 */

/* ───────────────────────────────────────────────────────────────
 *  ─ Schema bootstrap (idempotent) ──────────────────────────────
 * ─────────────────────────────────────────────────────────────── */
function fcm_ensure_schema(): void {
    static $done = false;
    if ($done) return;
    $done = true;

    $sql = <<<SQL
CREATE TABLE IF NOT EXISTS `fcm_tokens` (
  `device_id`      VARCHAR(64)  NOT NULL PRIMARY KEY,
  `token`          VARCHAR(4096) NOT NULL,
  `platform`       VARCHAR(16)  NOT NULL DEFAULT 'android',
  `app_version`    VARCHAR(32)           DEFAULT NULL,
  `user_id`        VARCHAR(64)           DEFAULT NULL,
  `is_active`      TINYINT(1)   NOT NULL DEFAULT 1,
  `created_at`     VARCHAR(40)  NOT NULL,
  `updated_at`     VARCHAR(40)  NOT NULL,
  `deactivated_at` VARCHAR(40)           DEFAULT NULL,
  INDEX `ix_fcm_tokens_active` (`is_active`),
  INDEX `ix_fcm_tokens_user`   (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `fcm_broadcasts` (
  `id`              INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `kind`            VARCHAR(16)  NOT NULL DEFAULT 'broadcast',
  `title`           VARCHAR(255) NOT NULL,
  `body`            TEXT         NOT NULL,
  `route`           VARCHAR(255)          DEFAULT NULL,
  `image_url`       VARCHAR(2000)         DEFAULT NULL,
  `action_label`    VARCHAR(64)           DEFAULT NULL,
  `target_user_id`  VARCHAR(64)           DEFAULT NULL,
  `total_tokens`    INT UNSIGNED NOT NULL DEFAULT 0,
  `sent`            INT UNSIGNED NOT NULL DEFAULT 0,
  `failed`          INT UNSIGNED NOT NULL DEFAULT 0,
  `pruned`          INT UNSIGNED NOT NULL DEFAULT 0,
  `last_error`      VARCHAR(255)          DEFAULT NULL,
  `last_detail`     VARCHAR(500)          DEFAULT NULL,
  `created_at`      VARCHAR(40)  NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL;
    foreach (explode(';', $sql) as $stmt) {
        $stmt = trim($stmt);
        if ($stmt !== '') db()->exec($stmt);
    }
    // Backfill columns for older installs (idempotent — ignored if exists).
    foreach ([
        "ALTER TABLE fcm_broadcasts ADD COLUMN `last_error`  VARCHAR(255) DEFAULT NULL",
        "ALTER TABLE fcm_broadcasts ADD COLUMN `last_detail` VARCHAR(500) DEFAULT NULL",
    ] as $ddl) {
        try { db()->exec($ddl); } catch (Throwable $e) { /* already exists */ }
    }
}

/* ───────────────────────────────────────────────────────────────
 *  ─ 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;
}

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';

    if (function_exists('apcu_fetch')) {
        $cached = apcu_fetch($cache_key, $ok);
        if ($ok && is_array($cached) && $cached['exp'] > time() + 60) return $cached['token'];
    }
    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'];
    }

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

    $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), '+/', '-_'), '=');

    $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'];
}

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', 'detail' => null];
    if ($code === 200) return ['ok' => true, 'status' => 200, 'error' => null, 'detail' => null];
    $j = json_decode((string)$resp, true);
    $err  = $j['error']['status']  ?? ('HTTP ' . $code);
    $msg  = $j['error']['message'] ?? null;
    // Google sometimes nests the precise reason under details[].errorCode.
    if (isset($j['error']['details']) && is_array($j['error']['details'])) {
        foreach ($j['error']['details'] as $d) {
            if (!empty($d['errorCode'])) { $err = (string)$d['errorCode']; break; }
        }
    }
    return ['ok' => false, 'status' => $code, 'error' => $err, 'detail' => $msg];
}

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_notification = [
        'channel_id' => 'default_notification_channel',
        // 'notification_priority' is the FCM v1 field name (HIGH was put under
        // android.priority above, which IS the correct place for top-level
        // delivery priority). Putting bare 'priority' here makes Google reject
        // the entire payload with 400 INVALID_ARGUMENT — exactly the bug we
        // hit in v1.2.3.
        'notification_priority' => 'PRIORITY_HIGH',
        // 'click_action' is valid in FCM v1 — it tells Android which intent
        // filter to launch when the user taps the notification.
        '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],
    ];
}

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

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');

    fcm_ensure_schema();
    $now = gmdate('c');

    $stmt = db()->prepare(
        'INSERT INTO fcm_tokens
            (device_id, token, platform, app_version, user_id, is_active, created_at, updated_at)
         VALUES (?, ?, ?, ?, ?, 1, ?, ?)
         ON DUPLICATE KEY UPDATE
            token = VALUES(token),
            platform = VALUES(platform),
            app_version = VALUES(app_version),
            user_id = VALUES(user_id),
            is_active = 1,
            updated_at = VALUES(updated_at),
            deactivated_at = NULL'
    );
    $stmt->execute([
        $device_id, $token,
        (string)($b['platform'] ?? 'android'),
        $b['app_version'] ?? null,
        $u['id'],
        $now, $now,
    ]);
    send_json(['registered' => true, 'device_id' => $device_id]);
}

function fcm_send_batch(array $rows, array $opts, string $kind): array {
    if (empty($rows)) {
        return ['queued' => false, 'total_tokens' => 0, 'sent' => 0, 'failed' => 0, 'pruned' => 0,
                'error' => 'No registered devices', 'last_error' => null, 'hint' => null];
    }
    $sa = fcm_service_account();
    $access = fcm_access_token();
    $message_proto = fcm_build_message($opts);

    $sent = 0; $failed = 0; $pruned = 0; $stale_ids = [];
    $last_error = null; $last_detail = null;
    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++;
        $last_error  = (string)$res['error'];
        $last_detail = $res['detail'] ?? null;
        $err = strtoupper($last_error);
        if (in_array($err, ['NOT_FOUND', 'UNREGISTERED', 'INVALID_ARGUMENT', 'SENDER_ID_MISMATCH'], true)
            || str_contains($err, 'NOT-REGISTERED')
            || str_contains($err, 'SENDERID')) {
            $stale_ids[] = $r['device_id'];
        }
    }
    if (!empty($stale_ids)) {
        $place = implode(',', array_fill(0, count($stale_ids), '?'));
        $stmt = db()->prepare(
            "UPDATE fcm_tokens SET is_active = 0, deactivated_at = ? WHERE device_id IN ($place)"
        );
        $stmt->execute(array_merge([gmdate('c')], $stale_ids));
        $pruned = count($stale_ids);
    }

    $stmt = db()->prepare(
        'INSERT INTO fcm_broadcasts
            (kind, title, body, route, image_url, action_label, target_user_id,
             total_tokens, sent, failed, pruned, last_error, last_detail, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
    );
    $stmt->execute([
        $kind,
        $opts['title'], $opts['body'],
        $opts['route']        ?? null,
        $opts['image_url']    ?? null,
        $opts['action_label'] ?? null,
        $opts['target_user_id'] ?? null,
        count($rows), $sent, $failed, $pruned,
        $last_error,
        $last_detail ? mb_substr((string)$last_detail, 0, 500) : null,
        gmdate('c'),
    ]);

    // Build a human-friendly hint when nothing landed on a device.
    $hint = null;
    if ($sent === 0 && $failed > 0) {
        $eu = strtoupper((string)$last_error);
        $project_id = $sa['project_id'] ?? '?';
        $detail_lc = strtolower((string)$last_detail);
        if (str_contains($detail_lc, 'invalid json payload')
            || str_contains($detail_lc, 'unknown name')
            || str_contains($detail_lc, 'cannot find field')) {
            // This is a SERVER-SIDE PAYLOAD BUG — wrong field name in the FCM
            // request, not anything the admin can fix in Firebase. The fix is
            // to update notifications.php on the server.
            $hint = "FCM payload format error — the server sent a field "
                  . "Google's v1 API doesn't recognise. This is a server "
                  . "code bug, NOT a Firebase project / token problem. "
                  . ($last_detail ? "Google said: \"$last_detail\". " : '')
                  . "Update your nadiplayer ZIP to the latest version, "
                  . "then click 'Reactivate pruned tokens' and try again.";
        } elseif (str_contains($eu, 'SENDER_ID_MISMATCH') || str_contains($eu, 'SENDERID')) {
            $hint = "SENDER ID MISMATCH — your APK's google-services.json belongs "
                  . "to a different Firebase project than this server's "
                  . "firebase-service-account.json (project_id = $project_id). "
                  . "Re-export both files from the SAME Firebase project, then rebuild the APK.";
        } elseif (in_array($eu, ['UNREGISTERED', 'NOT_FOUND'], true)) {
            $hint = 'All registered tokens were stale (app reinstalled or data cleared). '
                  . 'Reopen the app on at least one phone so it registers a fresh token, then try again.';
        } elseif ($eu === 'INVALID_ARGUMENT') {
            $hint = "INVALID_ARGUMENT — possible causes: (1) Firebase PROJECT MISMATCH "
                  . "(this server uses project_id = $project_id; your APK's "
                  . "google-services.json must contain the same project_id), "
                  . "or (2) malformed FCM payload from the server. "
                  . ($last_detail ? "Google said: \"$last_detail\"" : '');
        } else {
            $hint = "FCM returned: $last_error" . ($last_detail ? " — $last_detail" : '');
        }
    }

    return ['queued' => true, 'total_tokens' => count($rows),
            'sent' => $sent, 'failed' => $failed, 'pruned' => $pruned,
            'error' => null, 'last_error' => $last_error,
            'last_detail' => $last_detail, 'hint' => $hint,
            'project_id' => $sa['project_id'] ?? null];
}

function route_admin_broadcast(): void {
    require_admin();
    $b = read_json_body();
    fcm_ensure_schema();

    $rows = db()->query(
        'SELECT device_id, token FROM fcm_tokens WHERE is_active = 1'
    )->fetchAll();

    $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'));
}

function route_admin_notify_user(): void {
    require_admin();
    $b = read_json_body();
    fcm_ensure_schema();

    $user_id = trim((string)($b['user_id'] ?? $b['target_user_id'] ?? ''));
    if ($user_id === '') json_error(400, 'user_id is required');

    $stmt = db()->prepare(
        'SELECT device_id, token FROM fcm_tokens WHERE user_id = ? AND is_active = 1'
    );
    $stmt->execute([$user_id]);
    $rows = $stmt->fetchAll();

    $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'));
}

function route_admin_broadcasts(): void {
    require_admin();
    fcm_ensure_schema();
    $limit = max(1, min(200, (int)($_GET['limit'] ?? 20)));
    $stmt = db()->prepare(
        'SELECT title, body, route, image_url, action_label, target_user_id,
                total_tokens, sent, failed, pruned, last_error, last_detail, created_at, kind
         FROM fcm_broadcasts ORDER BY id DESC LIMIT ?'
    );
    $stmt->bindValue(1, $limit, PDO::PARAM_INT);
    $stmt->execute();
    send_json(['broadcasts' => $stmt->fetchAll()]);
}

function route_admin_devices(): void {
    require_admin();
    fcm_ensure_schema();
    $limit = max(1, min(200, (int)($_GET['limit'] ?? 50)));
    $skip  = max(0, (int)($_GET['skip']  ?? 0));

    $stmt = db()->prepare(
        'SELECT device_id, platform, app_version, user_id, is_active,
                created_at, updated_at
         FROM fcm_tokens ORDER BY updated_at DESC LIMIT ? OFFSET ?'
    );
    $stmt->bindValue(1, $limit, PDO::PARAM_INT);
    $stmt->bindValue(2, $skip,  PDO::PARAM_INT);
    $stmt->execute();
    $devices = $stmt->fetchAll();

    $total  = (int)db()->query('SELECT COUNT(*) FROM fcm_tokens')->fetchColumn();
    $active = (int)db()->query('SELECT COUNT(*) FROM fcm_tokens WHERE is_active = 1')->fetchColumn();

    send_json(['devices' => $devices, 'total' => $total, 'active' => $active]);
}
