-- +goose Up
-- Initial Game Lobby PostgreSQL schema.
--
-- Five tables cover the durable surface of the service:
--   * games, applications, invites, memberships — the four core
--     enrollment entities;
--   * race_names — the Race Name Directory, holding the registered /
--     reservation / pending_registration bindings keyed by canonical key.
--
-- Schema and the matching `lobbyservice` role are provisioned outside
-- this script (in tests via
-- integration/internal/harness/postgres_container.go::EnsureRoleAndSchema;
-- in production via an ops init script). This migration runs as the
-- schema owner with `search_path=lobby` and only contains DDL for the
-- service-owned tables and indexes.

-- games holds one durable record per platform game session. The status +
-- created_at index serves the listing/scheduler queries that previously
-- read `lobby:games_by_status:*`. The partial owner index serves the
-- per-owner listings used by user-lifecycle cascade and "my games"
-- listings; public games carry an empty owner_user_id and never enter
-- the index.
CREATE TABLE games (
    game_id               text PRIMARY KEY,
    game_name             text NOT NULL,
    description           text NOT NULL DEFAULT '',
    game_type             text NOT NULL,
    owner_user_id         text NOT NULL DEFAULT '',
    status                text NOT NULL,
    min_players           integer NOT NULL,
    max_players           integer NOT NULL,
    start_gap_hours       integer NOT NULL,
    start_gap_players     integer NOT NULL,
    enrollment_ends_at    timestamptz NOT NULL,
    turn_schedule         text NOT NULL,
    target_engine_version text NOT NULL,
    created_at            timestamptz NOT NULL,
    updated_at            timestamptz NOT NULL,
    started_at            timestamptz,
    finished_at           timestamptz,
    runtime_snapshot      jsonb NOT NULL DEFAULT '{}'::jsonb,
    runtime_binding       jsonb
);

CREATE INDEX games_status_created_idx
    ON games (status, created_at DESC, game_id DESC);

CREATE INDEX games_owner_idx
    ON games (owner_user_id) WHERE game_type = 'private';

-- applications carries one row per public-game enrollment request. The
-- partial UNIQUE on (applicant_user_id, game_id) WHERE status <> 'rejected'
-- replaces the Redis lookup key `lobby:user_game_application:*:*` and
-- enforces the single-active constraint at the database level. Rejected
-- applications are kept (one applicant may produce multiple rejected rows
-- before submitting a successful one).
CREATE TABLE applications (
    application_id     text PRIMARY KEY,
    game_id            text NOT NULL REFERENCES games(game_id) ON DELETE CASCADE,
    applicant_user_id  text NOT NULL,
    race_name          text NOT NULL,
    status             text NOT NULL,
    created_at         timestamptz NOT NULL,
    decided_at         timestamptz
);

CREATE INDEX applications_game_idx ON applications (game_id);

CREATE INDEX applications_user_idx ON applications (applicant_user_id);

CREATE UNIQUE INDEX applications_active_per_user_game_uidx
    ON applications (applicant_user_id, game_id)
    WHERE status <> 'rejected';

-- invites carries one row per private-game invitation. race_name is empty
-- until the invite transitions to redeemed. The (status, expires_at) index
-- serves the enrollment-automation expiration sweep; the per-game,
-- per-invitee, and per-inviter indexes serve listing queries from the
-- service layer.
CREATE TABLE invites (
    invite_id        text PRIMARY KEY,
    game_id          text NOT NULL REFERENCES games(game_id) ON DELETE CASCADE,
    inviter_user_id  text NOT NULL,
    invitee_user_id  text NOT NULL,
    race_name        text NOT NULL DEFAULT '',
    status           text NOT NULL,
    created_at       timestamptz NOT NULL,
    expires_at       timestamptz NOT NULL,
    decided_at       timestamptz
);

CREATE INDEX invites_game_idx     ON invites (game_id);
CREATE INDEX invites_invitee_idx  ON invites (invitee_user_id);
CREATE INDEX invites_inviter_idx  ON invites (inviter_user_id);
CREATE INDEX invites_status_expires_idx ON invites (status, expires_at);

-- memberships carries one row per platform roster entry. Both race_name
-- (original casing) and canonical_key are stored explicitly because
-- downstream readers (capability evaluation, cascade release) consume the
-- canonical form without re-deriving it from race_name. Race-name
-- uniqueness is enforced by the Race Name Directory (the race_names
-- table below) — this table intentionally has no unique constraint on
-- canonical_key.
CREATE TABLE memberships (
    membership_id  text PRIMARY KEY,
    game_id        text NOT NULL REFERENCES games(game_id) ON DELETE CASCADE,
    user_id        text NOT NULL,
    race_name      text NOT NULL,
    canonical_key  text NOT NULL,
    status         text NOT NULL,
    joined_at      timestamptz NOT NULL,
    removed_at     timestamptz
);

CREATE INDEX memberships_game_idx ON memberships (game_id);
CREATE INDEX memberships_user_idx ON memberships (user_id);

-- race_names is the durable Race Name Directory store. One row covers one
-- of three bindings on a canonical key: a registered name (one per
-- canonical_key, immutable holder), a per-game reservation, or a
-- pending_registration that is waiting on lobby.race_name.register inside
-- the eligible_until_ms window. The composite primary key (canonical_key,
-- game_id) lets the same user hold reservations for the same race name
-- across multiple active games concurrently, matching the behaviour the
-- shared port test suite (lobby/internal/ports/racenamedirtest) covers.
-- Registered rows store game_id = '' and keep the source game in
-- source_game_id so the per-canonical uniqueness rule expresses cleanly
-- as a partial UNIQUE index. Cross-user uniqueness on canonical_key is
-- enforced at write time inside transactions guarded by
-- pg_advisory_xact_lock(hashtextextended(canonical_key, 0)).
CREATE TABLE race_names (
    canonical_key      text   NOT NULL,
    game_id            text   NOT NULL DEFAULT '',
    holder_user_id     text   NOT NULL,
    race_name          text   NOT NULL,
    binding_kind       text   NOT NULL,
    source_game_id     text   NOT NULL DEFAULT '',
    reserved_at_ms     bigint NOT NULL DEFAULT 0,
    eligible_until_ms  bigint,
    registered_at_ms   bigint,
    PRIMARY KEY (canonical_key, game_id),
    CONSTRAINT race_names_binding_kind_chk
        CHECK (binding_kind IN ('registered', 'reservation', 'pending_registration'))
);

-- Exactly one registered binding per canonical_key. Reservations and
-- pending_registration entries are differentiated by game_id within the
-- primary key.
CREATE UNIQUE INDEX race_names_registered_uidx
    ON race_names (canonical_key)
    WHERE binding_kind = 'registered';

-- Per-user listings used by ListRegistered / ListReservations /
-- ListPendingRegistrations.
CREATE INDEX race_names_holder_idx
    ON race_names (holder_user_id, binding_kind);

-- Pending-registration expiration scanner reads only the pending subset
-- ordered by eligible_until_ms.
CREATE INDEX race_names_pending_eligible_idx
    ON race_names (eligible_until_ms)
    WHERE binding_kind = 'pending_registration';

-- +goose Down
DROP TABLE IF EXISTS race_names;
DROP TABLE IF EXISTS memberships;
DROP TABLE IF EXISTS invites;
DROP TABLE IF EXISTS applications;
DROP TABLE IF EXISTS games;
