-- UOMysticmoon database schema (MariaDB)
-- Run automatically by the MariaDB container (docker-entrypoint-initdb.d) on a
-- fresh volume, and idempotently by ensureSchema() on every server boot.

CREATE TABLE IF NOT EXISTS users (
  id            INT AUTO_INCREMENT PRIMARY KEY,
  username      VARCHAR(32)  NOT NULL UNIQUE,
  password_hash VARCHAR(72)  NOT NULL,
  role          ENUM('admin','editor') NOT NULL DEFAULT 'admin',
  totp_secret   VARCHAR(64)  NULL,          -- base32 TOTP secret (opt-in 2FA)
  totp_enabled  TINYINT(1)   NOT NULL DEFAULT 0,
  created_at    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  last_login_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS posts (
  id           INT AUTO_INCREMENT PRIMARY KEY,
  category     ENUM('news','five_on_friday','newsletter','screenshot') NOT NULL,
  title        VARCHAR(200) NOT NULL,
  slug         VARCHAR(220) NULL,
  excerpt      VARCHAR(400) NULL,
  body         MEDIUMTEXT   NULL,
  image_url    VARCHAR(500) NULL,
  published    TINYINT(1)   NOT NULL DEFAULT 0,
  author_id    INT NULL,
  created_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  published_at DATETIME NULL,
  CONSTRAINT fk_posts_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL,
  INDEX idx_posts_feed (category, published, published_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Wiki categories / sections. Defined before wiki_pages so the FK resolves on a
-- fresh install. Pages reference a category (nullable = "Uncategorized").
CREATE TABLE IF NOT EXISTS wiki_categories (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  slug        VARCHAR(120) NOT NULL UNIQUE,
  title       VARCHAR(200) NOT NULL,
  description VARCHAR(400) NULL,
  sort_order  INT NOT NULL DEFAULT 0,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS wiki_pages (
  id           INT AUTO_INCREMENT PRIMARY KEY,
  slug         VARCHAR(120) NOT NULL UNIQUE,
  title        VARCHAR(200) NOT NULL,
  body         MEDIUMTEXT   NULL,
  excerpt      VARCHAR(400) NULL,
  category_id  INT NULL,
  published    TINYINT(1)   NOT NULL DEFAULT 1,
  sort_order   INT NOT NULL DEFAULT 0,
  updated_by   INT NULL,
  created_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  published_at DATETIME NULL,
  CONSTRAINT fk_wiki_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
  CONSTRAINT fk_wiki_category FOREIGN KEY (category_id) REFERENCES wiki_categories(id) ON DELETE SET NULL,
  FULLTEXT INDEX idx_wiki_search (title, body)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Wiki tags (many-to-many with pages).
CREATE TABLE IF NOT EXISTS wiki_tags (
  id    INT AUTO_INCREMENT PRIMARY KEY,
  slug  VARCHAR(120) NOT NULL UNIQUE,
  label VARCHAR(120) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS wiki_page_tags (
  page_id INT NOT NULL,
  tag_id  INT NOT NULL,
  PRIMARY KEY (page_id, tag_id),
  CONSTRAINT fk_wpt_page FOREIGN KEY (page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE,
  CONSTRAINT fk_wpt_tag  FOREIGN KEY (tag_id)  REFERENCES wiki_tags(id)  ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Internal-link index, rebuilt on each save. target_slug may point at a page
-- that does not exist yet (a "red link").
CREATE TABLE IF NOT EXISTS wiki_links (
  source_page_id INT NOT NULL,
  target_slug    VARCHAR(120) NOT NULL,
  CONSTRAINT fk_wiki_links_src FOREIGN KEY (source_page_id) REFERENCES wiki_pages(id) ON DELETE CASCADE,
  INDEX idx_wiki_links_target (target_slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Per-save content snapshots for history / diff / restore.
CREATE TABLE IF NOT EXISTS wiki_revisions (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  page_id     INT NOT NULL,
  title       VARCHAR(200) NOT NULL,
  body        MEDIUMTEXT NULL,
  excerpt     VARCHAR(400) NULL,
  category_id INT NULL,
  editor_id   INT NULL,
  change_note VARCHAR(280) NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_wiki_rev_page   FOREIGN KEY (page_id)   REFERENCES wiki_pages(id) ON DELETE CASCADE,
  CONSTRAINT fk_wiki_rev_editor FOREIGN KEY (editor_id) REFERENCES users(id)      ON DELETE SET NULL,
  INDEX idx_wiki_rev_page (page_id, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS settings (
  `key`      VARCHAR(64) PRIMARY KEY,
  value      TEXT NULL,
  updated_by INT NULL,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  CONSTRAINT fk_settings_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS activity_log (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  user_id    INT NULL,
  action     VARCHAR(64) NOT NULL,
  detail     TEXT NULL,
  ip         VARCHAR(45) NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_activity_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
  INDEX idx_activity_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Pluggable SSO / OAuth2 provider configuration. Rows exist for the built-in
-- providers ('google', 'discord') once an admin configures them, plus any custom
-- OIDC/OAuth2 providers (id = a slug). Client secrets are stored ENCRYPTED
-- (client_secret_enc) and are never returned to a client. Built-in providers
-- hardcode their endpoint URLs in code; the *_url columns are used only by
-- custom (oidc/oauth2) providers.
CREATE TABLE IF NOT EXISTS auth_providers (
  id                VARCHAR(64) PRIMARY KEY,   -- 'google' | 'discord' | custom slug
  kind              ENUM('google','discord','oidc','oauth2') NOT NULL,
  name              VARCHAR(80)  NOT NULL,
  enabled           TINYINT(1)   NOT NULL DEFAULT 0,
  client_id         VARCHAR(255) NULL,
  client_secret_enc TEXT         NULL,         -- AES-256-GCM ciphertext, never exposed
  authorize_url     VARCHAR(500) NULL,         -- custom providers only
  token_url         VARCHAR(500) NULL,
  userinfo_url      VARCHAR(500) NULL,
  scopes            VARCHAR(500) NULL,
  priority          INT          NOT NULL DEFAULT 100,
  created_at        DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at        DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Account linking: maps an external SSO identity to an internal user. A login via
-- SSO succeeds only if a matching (provider, subject) row exists (link-only —
-- external identities are never auto-provisioned into accounts). UNIQUE(provider,
-- subject) guarantees one external identity maps to exactly one internal user.
CREATE TABLE IF NOT EXISTS user_identities (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  user_id    INT NOT NULL,
  provider   VARCHAR(64)  NOT NULL,            -- matches auth_providers.id
  subject    VARCHAR(191) NOT NULL,            -- external stable user id (sub / discord id)
  email      VARCHAR(255) NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_identity_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  UNIQUE KEY uq_identity_provider_subject (provider, subject),
  INDEX idx_identity_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Long-lived, revocable refresh tokens for mobile (Android) bearer-token auth.
-- The opaque refresh token is never stored in the clear — only its sha256 hash —
-- so a DB read does not leak usable tokens. Rows are rotated on every refresh
-- (old row revoked, new row inserted) and revoked on logout. Web cookie sessions
-- do NOT use this table; it is purely for the mobile bearer flow.
CREATE TABLE IF NOT EXISTS mobile_refresh_tokens (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  user_id     INT NOT NULL,
  token_hash  CHAR(64)     NOT NULL UNIQUE,   -- sha256 hex of the opaque refresh token
  device_hash VARCHAR(32)  NULL,              -- from sessionService.sessionMeta (best-effort)
  user_agent  VARCHAR(255) NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expires_at  DATETIME NOT NULL,
  revoked_at  DATETIME NULL,
  CONSTRAINT fk_mrt_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_mrt_user (user_id),
  INDEX idx_mrt_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Migrations for databases created before the wiki upgrade. Each statement uses
-- IF NOT EXISTS so re-running on every boot is a harmless no-op. New installs get
-- these columns from the CREATE TABLE above; existing installs get them here.
-- (The category foreign key is only added on fresh installs; on upgraded databases
-- referential integrity for category_id is enforced in application code.)
-- Opt-in TOTP two-factor columns for databases created before login hardening.
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_secret VARCHAR(64) NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_enabled TINYINT(1) NOT NULL DEFAULT 0;

ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS excerpt VARCHAR(400) NULL;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS category_id INT NULL;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published TINYINT(1) NOT NULL DEFAULT 1;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0;
ALTER TABLE wiki_pages ADD COLUMN IF NOT EXISTS published_at DATETIME NULL;
ALTER TABLE wiki_pages ADD FULLTEXT INDEX IF NOT EXISTS idx_wiki_search (title, body);
