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

// Long-lived credentials a user mints for themselves, so a native app can hold one instead of a password.
//
// The problem they solve: the mobile apps sign in and get a 30-day JWT, which means every app on every
// device is the same credential with the same expiry, and signing out of one is signing out of all of
// them. A key is per-app and per-device, revocable on its own, and does not expire underneath someone.
//
// ── What is stored ──
//
// `keyHash` is a SHA-256 of the secret and the secret itself is never written down — it is returned once,
// at creation, and is unrecoverable afterwards. SHA-256 rather than argon2 (which every password here
// uses) because the input is 32 bytes from a CSPRNG, not something a human chose: there is no dictionary
// to attack and nothing for a slow hash to buy, while a slow hash WOULD force a full table scan on every
// request, since we look a key up by its hash rather than by knowing whose it is. Same reasoning as
// GitHub's and Stripe's.
//
// `prefix` is the first few characters of the secret, kept in the clear purely so the UI can show which
// key is which. It is not enough to authenticate with.
//
// ── Authority ──
//
// A key currently carries its owner's full authority — deliberately, as the first cut. That is not a
// hole: it is exactly what the password that made it could already do, so a key is never an escalation.
// A `scopes` column is the intended next step, and the resolver is written so adding one touches the
// resolver and this table, nothing else.
export const apiKeys = pgTable(
  'api_keys',
  {
    id: serial('id').primaryKey(),
    userId: integer('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    /** What the user called it — "iPhone", "Music app on the iPad". Theirs to choose, shown back in lists. */
    name: text('name').notNull(),
    /** SHA-256 of the secret, hex. The lookup key: unique, so a collision is a constraint error not a mix-up. */
    keyHash: text('key_hash').notNull(),
    /** First characters of the secret, in the clear, for display only. */
    prefix: text('prefix').notNull(),
    /**
     * Updated on use, but at most once a minute — see touchApiKey. Without the debounce every
     * authenticated request would carry a write, which is a lot of churn to answer "is this still in use".
     */
    lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
    /** Null means it does not expire. Set at creation and never edited. */
    expiresAt: timestamp('expires_at', { withTimezone: true }),
    /** Set instead of deleting the row, so a revoked key's name and last use survive for the audit. */
    revokedAt: timestamp('revoked_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [uniqueIndex('uq_api_keys_key_hash').on(table.keyHash), index('idx_api_keys_user').on(table.userId)],
);
