-- §6.2 / v0.7.0: email + one-time-code sign-in.
--
-- Replaces the Gitea OAuth gesture as the primary human-auth path.
-- The Gitea bot user + token are still needed for server-side git
-- operations (repo reads, PR creation); only the operator-facing
-- sign-in surface moves. The /auth/callback OAuth route remains
-- functional during migration as a fallback, scheduled for removal
-- in a future release once every active user has signed in via OTC
-- at least once.
--
-- A row in `otc_codes` represents an outstanding 6-digit code that
-- was emailed to `email`. Codes are stored hashed (bcrypt) rather
-- than plaintext, so a database compromise does not expose the
-- in-flight code. TTL is enforced by `expires_at`. Each `verify`
-- success stamps `consumed_at` and refuses every later attempt
-- against the same row.
--
-- The §6.2 identity model under v0.7.0:
--
--   * `users.email` is the primary identity key for new sign-ins.
--   * `users.gitea_id` stays populated for users grandfathered in
--     via the OAuth-era flow; new users have `gitea_id = NULL`.
--     The unique-constraint on `gitea_id` is relaxed (in v0.5.0 it
--     was `INTEGER UNIQUE NOT NULL`) to permit the NULL.
--   * `users.email` becomes a (case-insensitive) unique key. An
--     existing OAuth user whose Gitea profile carried an email is
--     linked on first OTC sign-in; if no row matches, a fresh
--     contributor row is provisioned.
--
-- New env vars (v0.7.0):
--   * `OTC_TTL_MINUTES` (default 10): how long a code stays valid.
--   * `OTC_REQUEST_COOLDOWN_SECONDS` (default 60): per-email rate
--     limit between successive `/auth/otc/request` calls.

CREATE TABLE otc_codes (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    email           TEXT NOT NULL COLLATE NOCASE,
    code_hash       TEXT NOT NULL,
    created_at      TEXT NOT NULL DEFAULT (datetime('now')),
    expires_at      TEXT NOT NULL,
    consumed_at     TEXT
);

CREATE INDEX idx_otc_codes_email ON otc_codes (email, consumed_at, expires_at);

-- Relax `users.gitea_id` from `INTEGER UNIQUE NOT NULL` to a nullable
-- column with a partial unique index that ignores nulls. SQLite does
-- not support ALTER COLUMN, so we rebuild the table.
--
-- A few defensive notes:
--   * Every foreign key into `users(id)` continues to resolve — `id`
--     is the same INTEGER PRIMARY KEY in the rebuilt table.
--   * `email` is now declared NOCASE so a `WHERE email = ?` match
--     is case-insensitive without changing every read site. The
--     prior column accepted any text; existing rows pass through
--     unchanged.
--   * `gitea_login` likewise relaxes from NOT NULL to nullable, so
--     users provisioned by OTC alone don't carry a synthetic login.

CREATE TABLE users_new (
    id                                  INTEGER PRIMARY KEY AUTOINCREMENT,
    gitea_id                            INTEGER,
    gitea_login                         TEXT,
    email                               TEXT COLLATE NOCASE,
    display_name                        TEXT NOT NULL,
    avatar_url                          TEXT,
    role                                TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'contributor')),
    muted                               INTEGER NOT NULL DEFAULT 0,
    email_personal_direct               INTEGER NOT NULL DEFAULT 1,
    email_watched_structural            INTEGER NOT NULL DEFAULT 0,
    email_admin_actionable              INTEGER NOT NULL DEFAULT 1,
    email_opt_out_all                   INTEGER NOT NULL DEFAULT 0,
    digest_cadence                      TEXT NOT NULL DEFAULT 'weekly' CHECK (digest_cadence IN ('off', 'weekly', 'daily')),
    notification_quiet_hours_start      TEXT,
    notification_quiet_hours_end        TEXT,
    notification_quiet_hours_timezone   TEXT,
    created_at                          TEXT NOT NULL DEFAULT (datetime('now')),
    last_seen_at                        TEXT NOT NULL DEFAULT (datetime('now'))
);

INSERT INTO users_new (
    id, gitea_id, gitea_login, email, display_name, avatar_url, role,
    muted, email_personal_direct, email_watched_structural,
    email_admin_actionable, email_opt_out_all, digest_cadence,
    notification_quiet_hours_start, notification_quiet_hours_end,
    notification_quiet_hours_timezone, created_at, last_seen_at
)
SELECT
    id, gitea_id, gitea_login, email, display_name, avatar_url, role,
    muted, email_personal_direct, email_watched_structural,
    email_admin_actionable, email_opt_out_all, digest_cadence,
    notification_quiet_hours_start, notification_quiet_hours_end,
    notification_quiet_hours_timezone, created_at, last_seen_at
FROM users;

DROP TABLE users;
ALTER TABLE users_new RENAME TO users;

CREATE INDEX idx_users_role ON users (role);
-- Partial unique indexes so NULLs are permitted but populated values
-- collide. Gitea linkage stays unique per gitea_id; OTC-era identity
-- is keyed on email (case-insensitive via NOCASE on the column).
CREATE UNIQUE INDEX idx_users_gitea_id ON users (gitea_id) WHERE gitea_id IS NOT NULL;
CREATE UNIQUE INDEX idx_users_gitea_login ON users (gitea_login) WHERE gitea_login IS NOT NULL;
CREATE UNIQUE INDEX idx_users_email ON users (email) WHERE email IS NOT NULL AND email != '';
