/**
 * Identity and authorization.
 *
 * PIG authenticates with an external identity provider but authorizes from this
 * table. The distinction is load-bearing: the auth project may be shared with
 * other applications, so "has a valid token" must never imply "may use PIG".
 * A token gets you as far as `GET /api/me`, which returns 404 until an
 * administrator or a valid invite creates the row below.
 */
import {
  boolean,
  index,
  integer,
  jsonb,
  pgTable,
  primaryKey,
  text,
  timestamp,
  uniqueIndex,
  uuid,
} from 'drizzle-orm/pg-core';
import { teamEnum, teamRoleEnum } from './enums';

export const users = pgTable(
  'users',
  {
    id: uuid('id').primaryKey().defaultRandom(),

    /**
     * The external identity provider's subject claim. This is the join to
     * authentication and the only thing a bearer token proves.
     * Nullable so that a person can be invited before they first sign in.
     */
    authSubject: uuid('auth_subject'),

    email: text('email').notNull(),
    name: text('name').notNull(),
    /** Short handle used in mentions and CLI output. */
    handle: text('handle'),
    avatarUrl: text('avatar_url'),
    title: text('title'),
    timezone: text('timezone'),

    /**
     * Platform administration is separate from being a member of any team, and
     * separate again from being an employee of the organisation PIG tracks.
     * Conflating the three is how an operator quietly ends up recorded as staff
     * of a company they do not work for.
     */
    isPlatformAdmin: boolean('is_platform_admin').notNull().default(false),

    /**
     * Appearance preferences, persisted server-side rather than in
     * localStorage so a person's chosen look follows them between their
     * laptop and their phone. `system` defers to the OS.
     */
    themeMode: text('theme_mode').notNull().default('system'),
    /**
     * Accent colour key from the shared palette in @pig/core. The whole
     * interface re-tints from this one value. Stored as a key rather than a
     * hex string so the palette can be retuned centrally — and so a user
     * cannot pick something illegible against the surface colours.
     */
    accentColor: text('accent_color').notNull().default('pig'),

    /** Set when the person stops using PIG. Rows are retained for audit. */
    deactivatedAt: timestamp('deactivated_at', { withTimezone: true }),

    lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [
    uniqueIndex('users_auth_subject_key').on(t.authSubject),
    uniqueIndex('users_email_key').on(t.email),
    uniqueIndex('users_handle_key').on(t.handle),
  ],
);

/**
 * Team membership. A person may belong to several teams — in a small company
 * the same individual routinely sells capacity and sources it — so this is a
 * join table rather than a column on `users`.
 */
export const teamMemberships = pgTable(
  'team_memberships',
  {
    userId: uuid('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    team: teamEnum('team').notNull(),
    role: teamRoleEnum('role').notNull().default('member'),
    /** The team shown on sign-in when someone belongs to more than one. */
    isPrimary: boolean('is_primary').notNull().default(false),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [
    primaryKey({ columns: [t.userId, t.team] }),
    index('team_memberships_team_idx').on(t.team),
  ],
);

/**
 * Invitations. Self-serve profile creation is gated on one of these rather than
 * on the identity provider's own signup setting, which may be shared with
 * unrelated applications and toggled by someone with no knowledge of PIG.
 */
export const invites = pgTable(
  'invites',
  {
    id: uuid('id').primaryKey().defaultRandom(),
    /** Hashed, never stored in the clear. */
    codeHash: text('code_hash').notNull(),
    /** Optional pinning to one address; null allows any recipient. */
    email: text('email'),
    team: teamEnum('team'),
    role: teamRoleEnum('role').notNull().default('member'),
    createdByUserId: uuid('created_by_user_id').references(() => users.id, {
      onDelete: 'set null',
    }),
    expiresAt: timestamp('expires_at', { withTimezone: true }),

    /**
     * Times this invite may still be redeemed.
     *
     * An integer rather than JSON, because it is decremented in SQL and
     * arithmetic on a jsonb column does not work. A shared workspace code is
     * issued with a very large value; a per-person invite with 1.
     */
    usesRemaining: integer('uses_remaining').notNull().default(1),

    /**
     * Where this invite came from. `env:PIG_INVITE_CODE` marks the one
     * reconciled from configuration at boot, so rotating the variable can
     * revoke its predecessor without touching invites a human issued.
     */
    scopeNote: text('scope_note'),
    redeemedByUserId: uuid('redeemed_by_user_id').references(() => users.id, {
      onDelete: 'set null',
    }),
    redeemedAt: timestamp('redeemed_at', { withTimezone: true }),
    revokedAt: timestamp('revoked_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [
    uniqueIndex('invites_code_hash_key').on(t.codeHash),
    index('invites_email_idx').on(t.email),
  ],
);

/**
 * API keys, for the CLI and for MCP clients connecting over HTTP.
 *
 * An agent acting for a person is not the same principal as that person: it
 * gets its own key, its own audit trail, and can be revoked without disturbing
 * the human's session. Only the hash is stored, so a leaked database does not
 * yield working credentials.
 */
export const apiKeys = pgTable(
  'api_keys',
  {
    id: uuid('id').primaryKey().defaultRandom(),
    userId: uuid('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    name: text('name').notNull(),
    keyHash: text('key_hash').notNull(),
    /** Non-secret leading fragment, so a key can be identified in a list. */
    keyPrefix: text('key_prefix').notNull(),
    /** Coarse scopes. `read` is the default and is enough for most agent use. */
    scopes: jsonb('scopes').$type<string[]>().notNull().default(['read']),
    lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
    expiresAt: timestamp('expires_at', { withTimezone: true }),
    revokedAt: timestamp('revoked_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [
    uniqueIndex('api_keys_key_hash_key').on(t.keyHash),
    index('api_keys_user_idx').on(t.userId),
  ],
);

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type TeamMembership = typeof teamMemberships.$inferSelect;
export type ApiKey = typeof apiKeys.$inferSelect;
