-- Phase 3.5 users, roles, and bearer-token auth — see blueprint §11.6.
--
-- Lays down the schema that the unified can(principal, capability) gate
-- runs against, plus the api_keys table that backs `Authorization: Bearer
-- pic_…` credentials. No data-plane impact; Phase 4 SDKs (KV, docs, HTTP,
-- cron) will plug into this same authz pipeline.
--
-- Three changes:
--   1. admin_users gains instance_role ('owner'/'admin'/'member') plus a
--      reserved email column and mfa_secret slot (neither is read yet).
--      Every pre-existing row becomes 'owner' via the DEFAULT — Phase 3a
--      had no role concept, so promoting all current admins to owner is
--      the only safe interpretation (and matches the spec). The Rust
--      startup path logs a warning when more than one active owner
--      exists, so operators can demote extras via the admin PATCH.
--   2. app_members records explicit per-app grants for 'member' users.
--      Owners and admins get implicit grants in code (owner→app_admin
--      everywhere, admin→editor everywhere); no rows here.
--   3. api_keys holds Argon2id-hashed bearer credentials. Lookup is
--      prefix-indexed (first 8 chars after `pic_`) then hash-verified;
--      raw token only ever exists in the POST response. Optional
--      expires_at / app_id implement TTL and app-binding respectively.

ALTER TABLE admin_users
    -- DEFAULT 'owner' so the Phase 3a bootstrap admin (and any other
    -- pre-existing rows) become full owners without a backfill step.
    -- Multi-owner installs are flagged at startup; demotion is a
    -- deliberate PATCH, not an automatic migration choice.
    ADD COLUMN instance_role TEXT NOT NULL DEFAULT 'owner'
        CHECK (instance_role IN ('owner', 'admin', 'member')),
    -- Reserved for the eventual invite flow + Phase 4 user-management
    -- SDK. UNIQUE so we never end up with two rows claiming the same
    -- contact. Nullable because pre-existing admins have no email on
    -- file and we don't want to force a backfill.
    ADD COLUMN email TEXT UNIQUE,
    -- Reserved slot for TOTP secrets. Not read in Phase 3.5 — present
    -- now only to avoid a schema bump when MFA lands.
    ADD COLUMN mfa_secret TEXT;

CREATE INDEX admin_users_instance_role_idx ON admin_users (instance_role);

-- Per-(user, app) explicit grant. Owners and admins do NOT appear here;
-- their app authority is implicit in their instance_role and resolved in
-- code. Only 'member' users need rows in this table — without one, a
-- member has no access to the app at all.
CREATE TABLE app_members (
    app_id      UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
    user_id     UUID NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
    role        TEXT NOT NULL CHECK (role IN ('app_admin', 'editor', 'viewer')),
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (app_id, user_id)
);

-- Lookup pattern is "what apps can this user see?" — needed for the
-- membership-filtered GET /admin/apps and GET /admin/scripts.
CREATE INDEX app_members_user_id_idx ON app_members (user_id);

-- Bearer API keys. Format on the wire: `pic_<base32(32 random bytes)>`.
--   prefix = first 8 chars after `pic_` (indexed for O(1) candidate lookup)
--   hash   = Argon2id PHC of the full body after `pic_`
-- Raw value is returned exactly once at mint time and never persisted.
--
-- Optional fields:
--   expires_at: TTL. Lookup always filters `expires_at IS NULL OR > NOW()`.
--   app_id    : "bound key" — capability checks deny any App*(other_app),
--               regardless of the owning user's role. Cannot combine with
--               instance:* scopes (validated in the mint handler, not SQL).
CREATE TABLE api_keys (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id       UUID NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
    hash          TEXT NOT NULL,
    prefix        TEXT NOT NULL,
    name          TEXT NOT NULL,
    -- TEXT[] keeps the scope set open to additions without a migration;
    -- the seven legal values are validated at mint time in Rust, not by
    -- a CHECK constraint here (so new scopes can land without a schema
    -- bump).
    scopes        TEXT[] NOT NULL,
    app_id        UUID NULL REFERENCES apps(id) ON DELETE CASCADE,
    expires_at    TIMESTAMPTZ NULL,
    last_used_at  TIMESTAMPTZ NULL,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX api_keys_prefix_idx  ON api_keys (prefix);
CREATE INDEX api_keys_user_id_idx ON api_keys (user_id);

-- ---------------------------------------------------------------------
-- Reserved schema room (not built in Phase 3.5)
-- ---------------------------------------------------------------------
-- These tables are deliberately commented out, not created. They are
-- listed here so the design intent is visible at the migration boundary
-- and future authors don't reinvent the shape. Each lands in its own
-- numbered migration when the corresponding flow ships.
--
-- CREATE TABLE invites (
--     token         TEXT PRIMARY KEY,                     -- raw at email-link time, hashed at rest
--     email         TEXT NOT NULL,
--     instance_role TEXT NULL CHECK (instance_role IN ('owner','admin','member')),
--     app_id        UUID NULL REFERENCES apps(id) ON DELETE CASCADE,
--     app_role      TEXT NULL CHECK (app_role IN ('app_admin','editor','viewer')),
--     invited_by    UUID NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
--     expires_at    TIMESTAMPTZ NOT NULL,
--     consumed_at   TIMESTAMPTZ NULL
-- );
--
-- CREATE TABLE service_accounts (
--     id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
--     name           TEXT NOT NULL,
--     owning_user_id UUID NOT NULL REFERENCES admin_users(id) ON DELETE RESTRICT,
--     created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
-- );
