/**
 * Push router — VAPID public-key delivery + subscription lifecycle (NOTIF-01/02/03).
 *
 * Security (threat model T-05-09, T-05-10, T-05-13):
 *   - GET /vapid-public-key: public key only; sits under /api OIDC guard (PWA fetches post-login).
 *     The VAPID public key is non-secret — the private key is NEVER served.
 *   - POST /subscription: zod-validated body; userId from OIDC session (T-05-09 spoofing
 *     mitigation — client body never sets userId). Upsert on endpoint to handle re-subscribe.
 *   - DELETE /subscription: scoped WHERE userId = caller — cannot delete another member's
 *     subscription (T-05-13 access control).
 *
 * Mounted under /api/push in index.ts.
 */

import { Hono } from 'hono';
import type { Context } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { pushSubscriptions } from '../db/schema.js';
import { getAuth } from '../auth/middleware.js';
import { upsertUser, deriveDisplayName } from '../auth/user.js';
// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user')
import '../auth/devBypass.js';

export const pushRouter = new Hono();

// ---------------------------------------------------------------------------
// Auth helper — Duplicated per router (not extracted to shared module).
// Copied verbatim from lists.ts per project convention.
//
// Resolution order:
//   1. Dev-bypass path: c.get('user') is set by devAuthBypass() when DEV_AUTH_BYPASS=true.
//   2. OIDC path: call getAuth(c). If null → unauthenticated, return null.
// ---------------------------------------------------------------------------
async function resolveUserId(c: Context): Promise<number | null> {
  const devUser = c.get('user') as { id: number } | undefined;
  if (devUser) return devUser.id;

  const auth = await getAuth(c);
  if (!auth) return null;

  const iss = (auth.iss as string | undefined) ?? '';
  const sub = auth.sub ?? '';
  const displayName = deriveDisplayName(auth);
  const user = await upsertUser(iss, sub, displayName);
  return user?.id ?? null;
}

// ---------------------------------------------------------------------------
// Zod schemas
// ---------------------------------------------------------------------------

/**
 * Subscribe body — validated per T-05-10 (input validation).
 * Bounds: endpoint URL max 2048, p256dh max 512 (unpadded base64 of 65-byte P-256 key),
 * auth max 256 (unpadded base64 of 16-byte auth secret).
 */
const subscribeSchema = z.object({
  endpoint: z.string().url().max(2048),
  keys: z.object({
    p256dh: z.string().min(1).max(512),
    auth: z.string().min(1).max(256),
  }),
});

// ---------------------------------------------------------------------------
// GET /api/push/vapid-public-key
//
// Serves the VAPID public key to the PWA. The public key is not a secret — it
// is needed by the browser to compute the applicationServerKey for subscribe().
// The PRIVATE key is NEVER returned.
//
// Sits under /api OIDC guard (acceptable for v1 — only authenticated members can
// subscribe to push anyway).
// ---------------------------------------------------------------------------
pushRouter.get('/vapid-public-key', (c) => {
  return c.json({ publicKey: process.env.VAPID_PUBLIC_KEY ?? '' });
});

// ---------------------------------------------------------------------------
// POST /api/push/subscription
//
// Persists a push subscription row scoped to the authenticated user.
// Upserts on endpoint (unique constraint) so re-subscribing from the same device
// updates the ownership + keys without creating duplicate rows.
//
// Security: T-05-09 — userId derives from OIDC session, never from request body.
//           T-05-10 — zod subscribeSchema validates all fields before DB write.
// ---------------------------------------------------------------------------
pushRouter.post('/subscription', zValidator('json', subscribeSchema), async (c) => {
  const userId = await resolveUserId(c);
  if (!userId) return c.json({ error: 'Unauthorized' }, 401);

  const body = c.req.valid('json');

  try {
    await db
      .insert(pushSubscriptions)
      .values({
        userId,
        endpoint: body.endpoint,
        p256dh: body.keys.p256dh,
        auth: body.keys.auth,
      })
      .onDuplicateKeyUpdate({
        set: {
          userId,
          p256dh: body.keys.p256dh,
          auth: body.keys.auth,
        },
      });

    return c.json({ ok: true }, 201);
  } catch (err) {
    console.error(
      '[push/POST /subscription] DB operation failed:',
      err instanceof Error ? err.message : String(err),
    );
    return c.json({ error: 'Service unavailable' }, 503);
  }
});

// ---------------------------------------------------------------------------
// DELETE /api/push/subscription
//
// Removes all push subscription rows for the authenticated user.
// Scoped to caller only — cannot delete another member's subscriptions (T-05-13).
// ---------------------------------------------------------------------------
pushRouter.delete('/subscription', async (c) => {
  const userId = await resolveUserId(c);
  if (!userId) return c.json({ error: 'Unauthorized' }, 401);

  try {
    await db.delete(pushSubscriptions).where(eq(pushSubscriptions.userId, userId));
    return c.json({ ok: true });
  } catch (err) {
    console.error(
      '[push/DELETE /subscription] DB operation failed:',
      err instanceof Error ? err.message : String(err),
    );
    return c.json({ error: 'Service unavailable' }, 503);
  }
});
