import { eq, and, sql } from 'drizzle-orm';
import { db } from '../db';
import { pushDevices } from './schema';
import type { PushDeviceSelect, PushDeviceInsert } from '../types';

// The push device registry. Only the officer-notify sidecar uses these.

/**
 * Register a device, or refresh one already known. The app calls this on EVERY launch — push tokens
 * rotate silently, and a stale row is a device that stops receiving with no error anywhere — so this is
 * an upsert on (token, bundleId) rather than an insert, and it clears any accumulated failures.
 */
export async function upsertPushDevice(data: PushDeviceInsert): Promise<PushDeviceSelect> {
  const [row] = await db
    .insert(pushDevices)
    .values(data)
    .onConflictDoUpdate({
      target: [pushDevices.token, pushDevices.bundleId],
      set: {
        userId: data.userId,
        platform: data.platform,
        environment: data.environment ?? 'production',
        appSlug: data.appSlug,
        failureCount: 0,
        lastSeenAt: new Date(),
      },
    })
    .returning();
  return row!;
}

/** Every device for a user, optionally narrowed to one app. */
export async function getPushDevices(userId: number, appSlug?: string): Promise<PushDeviceSelect[]> {
  const where = appSlug
    ? and(eq(pushDevices.userId, userId), eq(pushDevices.appSlug, appSlug))
    : eq(pushDevices.userId, userId);
  return db.select().from(pushDevices).where(where);
}

/**
 * Sign-out, or a hard rejection from Apple/Google (410 Unregistered, UNREGISTERED).
 *
 * `userId` is optional because the two callers are genuinely different. The APNs/FCM paths delete a
 * token the provider has just declared dead, which is true for whoever owns it — they pass nothing. The
 * `DELETE /_officer/devices/:token` route is reached by a signed-in account naming a token in the URL,
 * so it MUST pass its own id: a token is the address of a device, not a secret, and without the
 * predicate any account could deregister another's device by guessing or replaying one.
 */
export async function deletePushDevice(token: string, userId?: number): Promise<void> {
  const where =
    userId === undefined
      ? eq(pushDevices.token, token)
      : and(eq(pushDevices.token, token), eq(pushDevices.userId, userId));
  await db.delete(pushDevices).where(where);
}

/**
 * A soft failure — a timeout, a 5xx. Three consecutive strikes and the row goes: without pruning, the
 * registry fills with dead tokens and delivery quietly degrades. Any successful send resets the count
 * via `upsertPushDevice`, so only *consecutive* failures accumulate.
 */
export async function recordPushFailure(token: string): Promise<void> {
  await db
    .update(pushDevices)
    .set({ failureCount: sql`${pushDevices.failureCount} + 1` })
    .where(eq(pushDevices.token, token));
  await db.delete(pushDevices).where(and(eq(pushDevices.token, token), sql`${pushDevices.failureCount} >= 3`));
}

/** A send worked: clear the failure count and mark the device alive. */
export async function markPushDeviceSeen(token: string): Promise<void> {
  await db.update(pushDevices).set({ failureCount: 0, lastSeenAt: new Date() }).where(eq(pushDevices.token, token));
}
