-- +goose Up
-- Stage 4 lobby & social: the friend graph, per-user blocks, per-game chat (with
-- nudge folded in as a message kind), email confirm-codes, and friend-game
-- invitations -- plus the per-game drop-out tile disposition the multi-player
-- engine needs. Matchmaking is an in-memory pool and persists nothing.
SET search_path = backend, pg_catalog;

-- The disposition of a dropped-out player's tiles in a game with three or more
-- seats (docs/ARCHITECTURE.md §6), chosen at creation: 'remove' burns them
-- (default), 'return' puts them back in the bag. Moot for a two-player game,
-- which ends on the first drop-out. engine.DropoutTiles owns the stable labels.
ALTER TABLE games
    ADD COLUMN dropout_tiles text NOT NULL DEFAULT 'remove',
    ADD CONSTRAINT games_dropout_tiles_chk CHECK (dropout_tiles IN ('remove', 'return'));

-- The friend graph. A row is created by the requester as 'pending' and flipped to
-- 'accepted' by the addressee; declining, cancelling or unfriending deletes the
-- row. Friendship is symmetric: a player's friends are the accepted rows in
-- either direction. A pair has at most one row (guarded in Go against either
-- direction existing).
CREATE TABLE friendships (
    requester_id uuid        NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
    addressee_id uuid        NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
    status       text        NOT NULL DEFAULT 'pending',
    created_at   timestamptz NOT NULL DEFAULT now(),
    responded_at timestamptz,
    PRIMARY KEY (requester_id, addressee_id),
    CONSTRAINT friendships_status_chk CHECK (status IN ('pending', 'accepted')),
    CONSTRAINT friendships_distinct_chk CHECK (requester_id <> addressee_id)
);
CREATE INDEX friendships_addressee_idx ON friendships (addressee_id);

-- Per-user blocks. blocker_id has blocked blocked_id; the effect is applied
-- mutually by the social checks (a block in either direction suppresses chat
-- visibility and prevents requests/invitations between the pair).
CREATE TABLE blocks (
    blocker_id uuid        NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
    blocked_id uuid        NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
    created_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (blocker_id, blocked_id),
    CONSTRAINT blocks_distinct_chk CHECK (blocker_id <> blocked_id)
);
CREATE INDEX blocks_blocked_idx ON blocks (blocked_id);

-- Per-game chat. A nudge ("it's your move") is a kind='nudge' row with an empty
-- body, so one journal carries both chatter and nudges. body is capped at 60
-- runes (enforced again in Go on input, where the content filter also rejects
-- links/emails/phone numbers). sender_ip holds the gateway-forwarded client IP as
-- a validated string (text, not inet, to avoid go-jet literal friction; the
-- gateway populates it in Stage 6). Chat is part of the game archive and is never
-- purged; it cascades away only with its game.
CREATE TABLE chat_messages (
    message_id uuid        PRIMARY KEY,
    game_id    uuid        NOT NULL REFERENCES games (game_id) ON DELETE CASCADE,
    sender_id  uuid        NOT NULL REFERENCES accounts (account_id),
    kind       text        NOT NULL DEFAULT 'message',
    body       text        NOT NULL DEFAULT '',
    sender_ip  text,
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT chat_messages_kind_chk CHECK (kind IN ('message', 'nudge')),
    CONSTRAINT chat_messages_body_len_chk CHECK (char_length(body) <= 60),
    CONSTRAINT chat_messages_nudge_empty_chk CHECK (kind <> 'nudge' OR body = '')
);
CREATE INDEX chat_messages_game_idx ON chat_messages (game_id, created_at);
-- Backs the once-per-hour nudge rate-limit lookup (latest nudge by a sender).
CREATE INDEX chat_messages_nudge_idx ON chat_messages (game_id, sender_id, created_at)
    WHERE kind = 'nudge';

-- Pending email confirm-codes. code_hash is the hex-encoded SHA-256 of the
-- 6-digit code (the plaintext is never stored, matching the session model);
-- expires_at bounds the TTL and attempts caps brute force. A row is consumed
-- (consumed_at stamped) on success. A re-request deletes the prior pending row
-- for the same (account, lowercased email) and inserts a fresh one.
CREATE TABLE email_confirmations (
    confirmation_id uuid        PRIMARY KEY,
    account_id      uuid        NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
    email           text        NOT NULL,
    code_hash       text        NOT NULL,
    expires_at      timestamptz NOT NULL,
    attempts        smallint    NOT NULL DEFAULT 0,
    consumed_at     timestamptz,
    created_at      timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT email_confirmations_attempts_chk CHECK (attempts >= 0)
);
CREATE INDEX email_confirmations_account_idx ON email_confirmations (account_id);

-- A friend-game invitation. The inviter (seat 0) proposes the game settings to
-- 1..3 invitees; the game starts only when every invitee has accepted, and any
-- decline cancels the whole invitation. Lazily expired after expires_at (no
-- background sweep). game_id is set when the game is started.
CREATE TABLE game_invitations (
    invitation_id     uuid        PRIMARY KEY,
    inviter_id        uuid        NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
    variant           text        NOT NULL,
    turn_timeout_secs integer     NOT NULL,
    hints_allowed     boolean     NOT NULL DEFAULT true,
    hints_per_player  smallint    NOT NULL DEFAULT 1,
    dropout_tiles     text        NOT NULL DEFAULT 'remove',
    status            text        NOT NULL DEFAULT 'pending',
    game_id           uuid        REFERENCES games (game_id) ON DELETE SET NULL,
    expires_at        timestamptz NOT NULL,
    created_at        timestamptz NOT NULL DEFAULT now(),
    updated_at        timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT game_invitations_variant_chk CHECK (variant IN ('english', 'russian_scrabble', 'erudit')),
    CONSTRAINT game_invitations_dropout_tiles_chk CHECK (dropout_tiles IN ('remove', 'return')),
    CONSTRAINT game_invitations_status_chk CHECK (status IN ('pending', 'declined', 'cancelled', 'expired', 'started')),
    CONSTRAINT game_invitations_turn_timeout_chk CHECK (turn_timeout_secs > 0),
    CONSTRAINT game_invitations_hints_per_player_chk CHECK (hints_per_player >= 0)
);
CREATE INDEX game_invitations_inviter_idx ON game_invitations (inviter_id);

-- One row per invitee (the inviter is implicit seat 0). seat is the invitee's
-- seat in the started game (1..3, in invitation order). response tracks each
-- invitee's pending/accepted/declined decision.
CREATE TABLE game_invitation_invitees (
    invitation_id uuid        NOT NULL REFERENCES game_invitations (invitation_id) ON DELETE CASCADE,
    account_id    uuid        NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
    seat          smallint    NOT NULL,
    response      text        NOT NULL DEFAULT 'pending',
    responded_at  timestamptz,
    PRIMARY KEY (invitation_id, account_id),
    CONSTRAINT game_invitation_invitees_response_chk CHECK (response IN ('pending', 'accepted', 'declined')),
    CONSTRAINT game_invitation_invitees_seat_chk CHECK (seat BETWEEN 1 AND 3)
);
CREATE INDEX game_invitation_invitees_account_idx ON game_invitation_invitees (account_id);

-- +goose Down
DROP TABLE game_invitation_invitees;
DROP TABLE game_invitations;
DROP TABLE email_confirmations;
DROP TABLE chat_messages;
DROP TABLE blocks;
DROP TABLE friendships;
ALTER TABLE games
    DROP CONSTRAINT games_dropout_tiles_chk,
    DROP COLUMN dropout_tiles;
