import { pgTable, serial, integer, text, timestamp, index, check, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from '../auth/schema';

// Devices that can receive a push, for the officer-notify sidecar.
//
// A token here is a NATIVE one — an APNs device token or an FCM registration token — because we send to
// Apple and Google directly rather than through Expo. See docs/push-notifications.md.
//
// Nothing sensitive is stored: a push token identifies a device's ability to receive, not the device or
// its owner, and it is useless without our APNs key or FCM service account. Those live in the sidecar's
// env and never touch this database. Notification CONTENT is never stored here either — payloads are a
// category and an id, assembled at send time and never retained.
//
// Only the officer-notify sidecar reads or writes this table.

const PLATFORMS = ['ios', 'android'] as const;
// APNs sandbox and production are different hosts AND different token namespaces: a token minted by a
// debug build fails against production with a silent BadDeviceToken. Android has no such split, but the
// column is not nullable — an android row is simply always 'production'.
const ENVIRONMENTS = ['production', 'sandbox'] as const;

export const pushDevices = pgTable(
  'push_devices',
  {
    id: serial('id').primaryKey(),
    userId: integer('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    token: text('token').notNull(),
    platform: text('platform').notNull(),
    environment: text('environment').notNull().default('production'),
    /** The `apns-topic`, and what distinguishes the three apps from each other. */
    bundleId: text('bundle_id').notNull(),
    /** `mobile` | `music` | `read-aloud` — for targeting a notification at one app. */
    appSlug: text('app_slug').notNull(),
    /** Consecutive send failures. Three strikes and the row is deleted; a hard rejection deletes at once. */
    failureCount: integer('failure_count').notNull().default(0),
    lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    // One row per token per app. The app re-registers on every launch because tokens rotate silently, so
    // registration is an upsert on this pair rather than an insert.
    uniqueIndex('uq_push_devices_token_bundle').on(table.token, table.bundleId),
    index('idx_push_devices_user').on(table.userId),
    // The schema is the source of truth for what may exist, not just its shape — see
    // src/databases/CLAUDE.md. sql.raw because an interpolated string binds as a parameter, which
    // Postgres refuses inside a CHECK and which breaks push for the whole schema.
    check(
      'ck_push_devices_platform',
      sql`${table.platform} IN (${sql.join(
        PLATFORMS.map((p) => sql.raw(`'${p}'`)),
        sql`, `,
      )})`,
    ),
    check(
      'ck_push_devices_environment',
      sql`${table.environment} IN (${sql.join(
        ENVIRONMENTS.map((e) => sql.raw(`'${e}'`)),
        sql`, `,
      )})`,
    ),
  ],
);
