-- §6.1 / v0.17.0: admin-create user with role + invite email (roadmap item #16).
--
-- Distinguishes from the v0.8.0 / v0.9.0 self-serve beta-access shape:
-- here an *admin* creates a `users` row *before* the invited person has
-- ever signed in, assigns them a role at creation time, and sends them
-- an invite email carrying a claim link. The invitee clicks the link,
-- the claim flow consumes the token (which is itself proof of email
-- control), the row is marked claimed, and the user is signed in
-- inheriting the pre-set role.
--
-- Migration slot 019 is allocated to this release. Slot 018 is reserved
-- for the parallel #12 release (per-RFC invitation, owner-only) shipping
-- in the same wave; the two features live in distinct tables
-- (`user_invite_tokens` here vs. `rfc_invitations` there) so they
-- coexist cleanly. Slot 016 was reserved+skipped by Session K during
-- v0.9.0 integration; slot 017 is the v0.11.0 device-trust table.
--
-- Open-question decisions settled in this release (see CHANGELOG):
--   * No `users` table changes — the brief floated `first_sign_in_at`
--     / `last_seen_at IS NULL` as the "(pending invite)" discriminator,
--     but the existing `users.last_seen_at` is NOT NULL with a
--     `datetime('now')` default (migrations/001) and there is no
--     `first_sign_in_at` column. Rather than land a schema migration to
--     introduce one, the discriminator is the existence of an active
--     (not-claimed, not-expired) row in `user_invite_tokens` joined on
--     `invited_user_id`. The admin user-listing carries a
--     `pending_invite` field populated via that join; on claim, the
--     invite row's `claimed_at` populates and the badge clears.
--     No new `permission_state` value is introduced either.
--   * The token is opaque (random URL-safe string, bcrypt-hashed at
--     rest), not a JWT, so admin revocation by row UPDATE works
--     without distributing a key-rotation gesture.
--   * The TTL is a constant (`INVITE_TOKEN_TTL_DAYS = 7` in
--     `backend/app/invites.py`); env-var configurability is a follow-up.
--   * Immediate-send (no admin-review-then-send queue) ships in this
--     release; admin-preview is a future enhancement.
--   * Bulk-invite (CSV paste) is deferred to a follow-up release;
--     v0.17.0 is one-at-a-time.
--
-- Storage shape:
--
--   * `id` — surrogate key. Lets the admin "pending invites" listing
--     address a row without leaking the token shape.
--   * `email` — the address the invite was sent to (case-insensitive
--     match at claim time, persisted verbatim for the audit trail).
--   * `role` — the role the invitee inherits on first sign-in. Pinned
--     via CHECK to the same set the §6.1 role flip accepts
--     (`owner` / `admin` / `contributor`) so a future role-set drift
--     fails loudly at insert rather than provisioning a ghost role.
--   * `first_name` / `last_name` — captured at create time so the
--     invitee skips the v0.8.0 capture-form step on first sign-in.
--   * `custom_message` — optional free-text from the admin (max 500
--     chars enforced at the API layer); embedded verbatim in the
--     email body if present.
--   * `token_hash` — bcrypt hash of the random opaque token. The
--     raw token only ever lives in the outbound email link and the
--     inbound claim body; server-side storage is the hash.
--   * `expires_at` — `created_at + 7 days` (default at the app layer
--     via `INVITE_TOKEN_TTL_DAYS`). A row past this stamp is dead;
--     the claim path refuses with HTTP 410.
--   * `created_at` — when the admin issued the invite.
--   * `created_by_admin_id` — FK into users(id) for the admin who
--     created the invite (no cascade; if the admin's row is deleted
--     the invite history stays so the audit trail survives).
--   * `claimed_at` — non-NULL once the invitee successfully claims.
--     A second claim attempt against an already-claimed row returns
--     HTTP 410.
--   * `claimed_by_user_id` — FK into users(id) for the user row
--     that consumed the token. In the common case this equals the
--     freshly-provisioned row that was created at invite time; the
--     FK lets the admin's "claimed" list join through.
--   * `invited_user_id` — FK into users(id) for the pre-provisioned
--     row. Created at invite time with `last_seen_at IS NULL` so the
--     v0.9.0 admin user-management page can render a "(pending
--     invite)" badge alongside existing users.
--
-- Indexing:
--   * Unique index on `token_hash` documents the no-collision
--     invariant (256 bits of CSPRNG entropy; collision is
--     structurally impossible, the unique constraint catches a
--     bug at insert time).
--   * Index on `(email, claimed_at)` so the "is this email already
--     invited?" pre-check the admin endpoint runs is a covering walk.
--   * Index on `(created_by_admin_id, created_at DESC)` for the
--     admin's "invites I've sent" listing.

CREATE TABLE user_invite_tokens (
    id                   INTEGER PRIMARY KEY AUTOINCREMENT,
    email                TEXT NOT NULL,
    role                 TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'contributor')),
    first_name           TEXT NOT NULL DEFAULT '',
    last_name            TEXT NOT NULL DEFAULT '',
    custom_message       TEXT NOT NULL DEFAULT '',
    token_hash           TEXT NOT NULL,
    expires_at           TEXT NOT NULL,
    created_at           TEXT NOT NULL DEFAULT (datetime('now')),
    created_by_admin_id  INTEGER NOT NULL REFERENCES users(id),
    claimed_at           TEXT,
    claimed_by_user_id   INTEGER REFERENCES users(id),
    invited_user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
);

CREATE UNIQUE INDEX idx_user_invite_tokens_hash ON user_invite_tokens (token_hash);
CREATE INDEX idx_user_invite_tokens_email ON user_invite_tokens (email, claimed_at);
CREATE INDEX idx_user_invite_tokens_admin ON user_invite_tokens (created_by_admin_id, created_at DESC);
