/**
 * pushDispatcher — VAPID-signed push send + 410/404 subscription prune.
 *
 * Single send path for all push triggers (reminders, list changes, event changes).
 * Centralises VAPID signing and dead-subscription pruning so individual triggers
 * never re-implement crypto or expiry handling.
 *
 * Threat mitigations (T-05-03, T-05-04, T-05-05):
 *   - Uses web-push library exclusively for VAPID signing (never hand-rolled).
 *   - Catches per-subscription errors; one failed send never aborts a fan-out loop.
 *   - Logs statusCode + err.message only — never logs subscription keys or payload body.
 *
 * Design notes:
 *   - Default import for web-push (CommonJS module — Pitfall 7 from RESEARCH.md).
 *   - setVapidDetails is NOT called here — it is called once from index.ts at startup.
 *   - dispatchPush resolves (never throws) so fan-out loops continue after failures.
 */

import webpush from 'web-push';
import { eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { pushSubscriptions } from '../db/schema.js';

/**
 * Push subscription row shape (subset of schema.pushSubscriptions used by dispatcher).
 */
export type PushSubscription = {
  id: number;
  userId: number;
  endpoint: string;
  p256dh: string;
  auth: string;
};

/**
 * Notification content passed to dispatchPush and used to build the push body.
 */
export type NotificationPayload = {
  title: string;
  body?: string;
  tag?: string;
  navigate?: string;
};

/**
 * Build the dual-format push payload body string.
 *
 * Carries both:
 *   1. iOS 18.4+ declarative web push (web_push:8030 + notification object)
 *   2. Legacy title/body/tag/data for iOS 16.4–18.3 and Android
 *
 * The service worker reads whichever format the browser understands.
 */
export function buildPushBody(notification: NotificationPayload): string {
  const { title, body = '', tag, navigate } = notification;

  return JSON.stringify({
    // iOS 18.4+ declarative web push format (WebKit blog 2025-04-14)
    web_push: 8030,
    notification: {
      title,
      body,
      navigate,
    },
    // Legacy format for iOS 16.4–18.3 and Android
    title,
    body,
    ...(tag !== undefined && { tag }),
    data: {
      url: navigate,
    },
  });
}

/**
 * Sign and dispatch a single push notification to one subscription.
 *
 * On 410 (Gone) or 404 (Not Found) response from the push service:
 *   - The subscription row is deleted from push_subscriptions (D-11 prune).
 *
 * On transient errors (5xx, 429, network failures):
 *   - NO delete is performed.
 *   - The error is logged with the [pushDispatcher] prefix.
 *   - The function resolves (never throws to caller).
 *
 * On success:
 *   - No action beyond the send.
 */
export async function dispatchPush(
  sub: PushSubscription,
  notification: NotificationPayload,
): Promise<void> {
  const webPushSub = {
    endpoint: sub.endpoint,
    keys: {
      p256dh: sub.p256dh,
      auth: sub.auth,
    },
  };

  const body = buildPushBody(notification);

  try {
    await webpush.sendNotification(webPushSub, body, {
      TTL: 300,
      urgency: 'normal',
    });
  } catch (err: unknown) {
    const statusCode = (err as { statusCode?: number }).statusCode;

    if (statusCode === 410 || statusCode === 404) {
      // Dead subscription — prune from DB (D-11)
      await db.delete(pushSubscriptions).where(eq(pushSubscriptions.id, sub.id));
      return;
    }

    // Transient error — log and continue; do NOT delete subscription
    const message = err instanceof Error ? err.message : String(err);
    console.error('[pushDispatcher] sendNotification failed:', statusCode, message);
  }
}
