-- §6 / §10 / v0.16.0: owner-only invite for per-RFC contribution +
-- discussion (roadmap item #12).
--
-- Distinct from a platform-level grant (`users.permission_state`,
-- v0.8.0 / item #6). This row is per-RFC membership: the RFC's owner
-- invites a specific email to either open PRs against that RFC
-- (`role_in_rfc='contributor'`) or to participate in the RFC's PR-less
-- discussion only (`role_in_rfc='discussant'`). Non-invited users keep
-- the v0.6.0 anonymous-read contract — they can read but cannot
-- write/discuss that specific RFC.
--
-- Coordinates with item #16's parallel work this wave: that item
-- adds platform-wide invitation tokens; this one adds per-RFC
-- collaboration rows. To avoid table-name + concept collisions the
-- two surfaces are scoped distinctly — this migration owns slot 018
-- and names everything `rfc_*` (RFC-scoped); #16 will use a later
-- slot and name its tables under a different prefix (`invite_tokens`
-- or similar) at the user/platform level.
--
-- Tables in this migration:
--
--   * `rfc_invitations` — one row per (rfc, invitee_email) invite
--     issued by the RFC's owner. Carries the role-in-RFC the
--     invitation grants, the opaque token the email link encodes,
--     the lifecycle state, and the audit trail (who invited, when
--     accepted, by which user_id if any).
--
--   * `rfc_collaborators` — one row per (rfc, user_id, role_in_rfc)
--     after an invitation is accepted. This is the table the
--     write-gate consults: "is the viewer named here for this RFC?"
--     Separating the two means the invitation row carries the
--     issue/accept lifecycle while the collaborator row is the
--     compact membership-check substrate. A grant via collaborator
--     can exist independently of a live invitation (admin-only
--     direct insert is a §19.2 candidate; v0.16.0 only writes
--     collaborator rows via the accept path).
--
-- Authorization model the application layer enforces on top of these
-- rows (not encoded in SQL — the schema is just storage):
--
--   * Writes (open PR, post discussion message, open discussion
--     thread) to an RFC require ONE of:
--       (a) the viewer is named in this RFC's `rfc_collaborators`
--           with the appropriate role_in_rfc, OR
--       (b) the viewer holds a globally privileged role (admin,
--           owner of the platform) per the existing §6 helpers, OR
--       (c) the viewer is named in the RFC's frontmatter owners
--           list (the §6 RFC-owner concept, which already grants
--           the maximal per-RFC capability).
--
--   * Reads remain on the v0.6.0 anonymous-read contract — anyone
--     can read any non-withdrawn RFC. Item #12 does not narrow this.
--
--   * Only the RFC's owner (per `cached_rfcs.owners_json`) can
--     invite. App admins/owners also can (they have the maximal
--     per-RFC capability by construction).
--
-- Storage shape — `rfc_invitations`:
--
--   * `id` — surrogate key; the revoke-by-id surface addresses a
--     single row without leaking the token shape.
--
--   * `rfc_slug` — TEXT NOT NULL; the RFC the invitation scopes to.
--     We FK against `cached_rfcs(slug)` so a withdrawn/deleted RFC
--     cascades its invitations away cleanly. The §4 cache contract
--     says cached_rfcs is rebuildable from Gitea; per the same
--     contract, invitations are app-truth (no Git substrate), so
--     the cascade is the right direction.
--
--   * `inviter_user_id` — the owner who issued the invite. ON
--     DELETE SET NULL because losing the inviter's user row should
--     not cascade-delete invitations they sent (the row stays as
--     audit; the UI renders "by (deleted user)" the same way the
--     audit log does for orphaned actors).
--
--   * `invitee_email` — TEXT NOT NULL; the email the invitation
--     was sent to. Stored verbatim (case-preserved) so the email
--     body can address the invitee in their original shape; the
--     accept path matches case-insensitively.
--
--   * `role_in_rfc` — CHECK in {'contributor' | 'discussant'}.
--     `contributor` lets the user open PRs against the RFC AND
--     post in its discussion (PR-permission strictly includes
--     discussion-permission); `discussant` only lets them post
--     in discussion. Future roles (e.g., 'arbiter') would be
--     additions; v0.16.0 ships the two.
--
--   * `status` — CHECK in {'pending' | 'accepted' | 'revoked' |
--     'expired'}. Default 'pending'. `accepted` flips on the
--     accept endpoint; `revoked` on the owner's revoke gesture;
--     `expired` lazily on read (the accept endpoint refuses a
--     row whose expires_at has passed, regardless of the column
--     value).
--
--   * `token` — opaque high-entropy string the email link
--     encodes. Stored verbatim (not hashed) because the
--     invitation token is single-use and lower-stakes than a
--     session token: it grants per-RFC role only, and is bounded
--     by expires_at. Hashing the token here is a §19.2 candidate
--     if/when the threat model demands it. UNIQUE so the accept
--     path is a single-row lookup.
--
--   * `expires_at` — TEXT timestamp. Set to `created_at + 30 days`
--     at insert time by the application layer. Accept refuses past
--     this point; the row can still be revoked or re-issued.
--
--   * `created_at` — when the invitation was issued.
--
--   * `accepted_at` — when the invitee accepted (NULL until then).
--
--   * `accepted_by_user_id` — the user row that accepted. NULL
--     until acceptance. On a fresh email (no platform user yet)
--     the accept endpoint requires the invitee to sign in first
--     via the v0.7.0 OTC path; that path provisions the user row,
--     after which the accept call lands the user_id here.
--
-- Indexing:
--
--   * UNIQUE on `token` so the accept lookup is a primary-key-shape
--     hit and accidental collisions are detectable at insert time.
--   * (rfc_slug, status) for the owner's "list pending/accepted for
--     this RFC" surface — the most frequent query.
--   * (invitee_email, status) for a future cross-RFC "show me my
--     pending invites" inbox; v0.16.0 doesn't ship that surface but
--     the index slot is cheap and aligned with the data shape.
--
-- Storage shape — `rfc_collaborators`:
--
--   * `id` — surrogate key.
--   * `rfc_slug` — TEXT NOT NULL FK cached_rfcs(slug) ON DELETE CASCADE.
--   * `user_id` — INTEGER NOT NULL FK users(id) ON DELETE CASCADE.
--     A deleted user loses every per-RFC role automatically (mirrors
--     the device_trust / passcode cascade shape).
--   * `role_in_rfc` — same CHECK as the invitation table.
--   * `invitation_id` — INTEGER FK rfc_invitations(id) ON DELETE
--     SET NULL. Audit pointer to the row that minted this
--     collaborator; NULL is allowed so a future admin-direct grant
--     path (a §19.2 candidate) can mint a collaborator with no
--     originating invitation. v0.16.0 always populates this.
--   * `created_at` — when the collaborator row was minted.
--
-- Indexing on collaborators:
--   * UNIQUE on (rfc_slug, user_id) — a single user can hold at most
--     one role per RFC. Re-accepting an invitation upgrades the row
--     (discussant → contributor) but never duplicates.
--   * (user_id) for "what RFCs am I a collaborator on?" reads.

CREATE TABLE rfc_invitations (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT,
    rfc_slug                TEXT NOT NULL REFERENCES cached_rfcs(slug) ON DELETE CASCADE,
    inviter_user_id         INTEGER REFERENCES users(id) ON DELETE SET NULL,
    invitee_email           TEXT NOT NULL,
    role_in_rfc             TEXT NOT NULL CHECK (role_in_rfc IN ('contributor', 'discussant')),
    status                  TEXT NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')),
    token                   TEXT NOT NULL,
    expires_at              TEXT NOT NULL,
    created_at              TEXT NOT NULL DEFAULT (datetime('now')),
    accepted_at             TEXT,
    accepted_by_user_id     INTEGER REFERENCES users(id) ON DELETE SET NULL
);

CREATE UNIQUE INDEX idx_rfc_invitations_token ON rfc_invitations (token);
CREATE INDEX idx_rfc_invitations_rfc_status ON rfc_invitations (rfc_slug, status);
CREATE INDEX idx_rfc_invitations_email_status ON rfc_invitations (invitee_email, status);

CREATE TABLE rfc_collaborators (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    rfc_slug        TEXT NOT NULL REFERENCES cached_rfcs(slug) ON DELETE CASCADE,
    user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role_in_rfc     TEXT NOT NULL CHECK (role_in_rfc IN ('contributor', 'discussant')),
    invitation_id   INTEGER REFERENCES rfc_invitations(id) ON DELETE SET NULL,
    created_at      TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE UNIQUE INDEX idx_rfc_collaborators_unique ON rfc_collaborators (rfc_slug, user_id);
CREATE INDEX idx_rfc_collaborators_user ON rfc_collaborators (user_id);
