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

// What a ROLE may reach. The subject of a grant is a role, never a user.
//
// That is the owner's explicit call (2026-08-06) and it is worth stating why, because per-user permissions
// look more flexible and are the obvious next request. Per-user grants make "what can this person do" a
// question you answer by reading rows, and "what can a Member do" a question with no answer at all. Roles
// keep both answerable: onboarding is picking a role, and auditing is reading one short table. When the
// answer really has to differ for one person, that person needs a role, and adding one is a line in
// USER_ROLES.
//
// A MISSING ROW MEANS NO ACCESS. There is no row that denies; absence denies. This is what makes the table
// safe to read as the whole truth — an empty table is a platform where members can reach nothing but their
// own account, which is the correct state for a server that has just been installed.
//
// `capability` is a key from src/servers/capabilities/registry.ts and is deliberately NOT constrained here.
// A CHECK listing the keys would put the registry in two places and make adding a capability a schema
// change; worse, renaming one would fail the push rather than telling you what actually broke. The registry
// is the source of truth and the API layer rejects unknown keys on write. What the database does enforce is
// the shape: a legal role, a legal level, and at most one grant per pair.
const CAPABILITY_LEVELS = ['read', 'write'] as const;
export type CapabilityLevelValue = (typeof CAPABILITY_LEVELS)[number];

export const roleCapabilities = pgTable(
  'role_capabilities',
  {
    id: serial('id').primaryKey(),
    role: text('role', { enum: USER_ROLES }).notNull(),
    /** A capability key from the registry. Validated by the API, not by a constraint — see above. */
    capability: text('capability').notNull(),
    /**
     * `read` permits safe methods plus mutations under the capability's `personal` sub-paths — the ones
     * holding the caller's own data. `write` is unconditional within the capability.
     */
    level: text('level', { enum: CAPABILITY_LEVELS }).notNull().default('read'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    // uniqueIndex, not unique().on() — drizzle-kit mis-diffs named composite unique CONSTRAINTS and
    // re-creates them on every push, which stops db:push on an unanswerable truncate prompt. See
    // src/databases/CLAUDE.md → "Composite keys".
    uniqueIndex('uq_role_capabilities_role_capability').on(table.role, table.capability),
    // Postgres will not reject an unknown string in a text column just because TypeScript narrowed it, so
    // the legal sets are stated here too. sql.raw for the literals: an interpolated JS string binds as a
    // parameter and Postgres refuses a parameter inside a CHECK, which breaks push for the WHOLE schema.
    check(
      'ck_role_capabilities_role',
      sql`${table.role} IN (${sql.join(
        USER_ROLES.map((r) => sql.raw(`'${r}'`)),
        sql`, `,
      )})`,
    ),
    check(
      'ck_role_capabilities_level',
      sql`${table.level} IN (${sql.join(
        CAPABILITY_LEVELS.map((l) => sql.raw(`'${l}'`)),
        sql`, `,
      )})`,
    ),
    // The owner is not granted anything, because the owner is not gated by this table at all — isSuperAdmin
    // short-circuits every check. A 'Super Admin' row here would be inert, and an inert row that looks
    // meaningful is worse than no row: someone would eventually revoke one and expect it to do something.
    check('ck_role_capabilities_not_owner', sql`${table.role} <> ${sql.raw("'Super Admin'")}`),
  ],
);
