-- Chattyness Auth Schema Tables
-- PostgreSQL 18
--
-- User authentication, accounts, and identity management

\set ON_ERROR_STOP on

BEGIN;

-- =============================================================================
-- User Accounts
-- =============================================================================

CREATE TABLE auth.users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    username auth.username NOT NULL,
    email auth.email,
    phone TEXT,  -- TODO: migrate to auth.phone_number domain
    password_hash TEXT,
    auth_provider auth.auth_provider NOT NULL DEFAULT 'local',
    oauth_id TEXT,

    display_name public.display_name NOT NULL,
    bio TEXT,
    avatar_url public.url,

    -- HOSS membership profile fields
    name_first TEXT,
    name_last TEXT,
    summary TEXT,                                          -- Brief one-liner tagline
    homepage public.url,
    avatar_source auth.avatar_source NOT NULL DEFAULT 'local',
    profile_visibility auth.profile_visibility NOT NULL DEFAULT 'public',
    contacts_visibility auth.profile_visibility NOT NULL DEFAULT 'members',
    organizations_visibility auth.profile_visibility NOT NULL DEFAULT 'members',

    -- User preferences for default avatar selection
    birthday DATE,
    gender_preference auth.gender_preference NOT NULL DEFAULT 'gender_neutral',
    age_category auth.age_category NOT NULL DEFAULT 'adult',

    reputation_tier server.reputation_tier NOT NULL DEFAULT 'member',
    reputation_promoted_at TIMESTAMPTZ,

    status auth.account_status NOT NULL DEFAULT 'active',
    email_verified BOOLEAN NOT NULL DEFAULT false,
    email_verified_at TIMESTAMPTZ,
    force_pw_reset BOOLEAN NOT NULL DEFAULT false,

    last_seen_at TIMESTAMPTZ,
    total_time_online_seconds BIGINT NOT NULL DEFAULT 0,

    script_state JSONB NOT NULL DEFAULT '{}',

    -- User tags for feature gating and access control
    tags auth.user_tag[] NOT NULL DEFAULT '{}',

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_users_username UNIQUE (username),
    CONSTRAINT uq_auth_users_email UNIQUE (email),
    CONSTRAINT uq_auth_users_oauth UNIQUE (auth_provider, oauth_id),
    CONSTRAINT chk_auth_users_password_or_oauth_or_guest
        CHECK (password_hash IS NOT NULL OR oauth_id IS NOT NULL OR 'guest' = ANY(tags))
);

COMMENT ON TABLE auth.users IS 'User accounts and authentication';

CREATE INDEX idx_auth_users_email ON auth.users (lower(email));
CREATE INDEX idx_auth_users_status ON auth.users (status);
CREATE INDEX idx_auth_users_reputation ON auth.users (reputation_tier);
CREATE INDEX idx_auth_users_last_seen ON auth.users (last_seen_at DESC NULLS LAST);
CREATE INDEX idx_auth_users_tags ON auth.users USING GIN (tags);

-- =============================================================================
-- User Sessions
-- =============================================================================

CREATE TABLE auth.sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    token_hash TEXT NOT NULL,
    user_agent TEXT,
    ip_address INET,

    expires_at TIMESTAMPTZ NOT NULL,
    last_activity_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_sessions_token UNIQUE (token_hash)
);

COMMENT ON TABLE auth.sessions IS 'Active user sessions';

CREATE INDEX idx_auth_sessions_user ON auth.sessions (user_id);
CREATE INDEX idx_auth_sessions_expires ON auth.sessions (expires_at);

-- =============================================================================
-- Tower Sessions (tower-sessions crate)
-- =============================================================================

CREATE TABLE auth.tower_sessions (
    id TEXT PRIMARY KEY,
    data BYTEA NOT NULL,
    expiry_date TIMESTAMPTZ NOT NULL
);

COMMENT ON TABLE auth.tower_sessions IS 'Session storage for tower-sessions crate';

CREATE INDEX idx_auth_tower_sessions_expiry ON auth.tower_sessions (expiry_date);

-- =============================================================================
-- Friends List
-- =============================================================================

CREATE TABLE auth.friendships (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    friend_a UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    friend_b UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    initiated_by UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    is_accepted BOOLEAN NOT NULL DEFAULT false,
    accepted_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_friendships UNIQUE (friend_a, friend_b),
    CONSTRAINT chk_auth_friendships_ordered CHECK (friend_a < friend_b),
    CONSTRAINT chk_auth_friendships_initiator CHECK (initiated_by = friend_a OR initiated_by = friend_b)
);

COMMENT ON TABLE auth.friendships IS 'Friend relationships (normalized: friend_a < friend_b)';

CREATE INDEX idx_auth_friendships_friend_a ON auth.friendships (friend_a);
CREATE INDEX idx_auth_friendships_friend_b ON auth.friendships (friend_b);
CREATE INDEX idx_auth_friendships_pending ON auth.friendships (is_accepted) WHERE is_accepted = false;

-- =============================================================================
-- User Contacts (Social/Contact Links)
-- =============================================================================

CREATE TABLE auth.user_contacts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    platform auth.contact_platform NOT NULL,
    value TEXT NOT NULL,
    label TEXT,                                            -- Optional display label
    sort_order SMALLINT NOT NULL DEFAULT 0,

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_user_contacts_platform UNIQUE (user_id, platform, value),
    CONSTRAINT chk_auth_user_contacts_value_nonempty CHECK (length(trim(value)) > 0)
);

COMMENT ON TABLE auth.user_contacts IS 'User social/contact platform links';
COMMENT ON COLUMN auth.user_contacts.platform IS 'Type of contact (discord, github, linkedin, etc.)';
COMMENT ON COLUMN auth.user_contacts.value IS 'Platform-specific identifier (handle, URL, phone, etc.)';
COMMENT ON COLUMN auth.user_contacts.label IS 'Optional user-friendly display label';
COMMENT ON COLUMN auth.user_contacts.sort_order IS 'Display order (lower = first)';

CREATE INDEX idx_auth_user_contacts_user ON auth.user_contacts (user_id);
CREATE INDEX idx_auth_user_contacts_platform ON auth.user_contacts (platform);

-- =============================================================================
-- User Organizations (Affiliations)
-- =============================================================================

CREATE TABLE auth.user_organizations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    wikidata_qid public.wikidata_qid,                      -- Wikidata Q-number for org
    name TEXT NOT NULL,                                    -- Display name (may differ from Wikidata)
    role TEXT,                                             -- User's role/title
    department TEXT,                                       -- Department/division
    start_date DATE,
    end_date DATE,
    is_current BOOLEAN NOT NULL DEFAULT true,
    sort_order SMALLINT NOT NULL DEFAULT 0,

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT chk_auth_user_orgs_name_nonempty CHECK (length(trim(name)) > 0),
    CONSTRAINT chk_auth_user_orgs_dates CHECK (start_date IS NULL OR end_date IS NULL OR start_date <= end_date)
);

COMMENT ON TABLE auth.user_organizations IS 'User organizational affiliations';
COMMENT ON COLUMN auth.user_organizations.wikidata_qid IS 'Wikidata Q-number for standardized organization lookup';
COMMENT ON COLUMN auth.user_organizations.name IS 'Display name for the organization';
COMMENT ON COLUMN auth.user_organizations.role IS 'User role/title at the organization';
COMMENT ON COLUMN auth.user_organizations.is_current IS 'Whether this is a current affiliation';

CREATE INDEX idx_auth_user_organizations_user ON auth.user_organizations (user_id);
CREATE INDEX idx_auth_user_organizations_wikidata ON auth.user_organizations (wikidata_qid)
    WHERE wikidata_qid IS NOT NULL;
CREATE INDEX idx_auth_user_organizations_current ON auth.user_organizations (user_id, is_current)
    WHERE is_current = true;

-- =============================================================================
-- Block List
-- =============================================================================

CREATE TABLE auth.blocks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    blocker_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    blocked_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    reason TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_blocks UNIQUE (blocker_id, blocked_id),
    CONSTRAINT chk_auth_blocks_not_self CHECK (blocker_id != blocked_id)
);

COMMENT ON TABLE auth.blocks IS 'User block list';

CREATE INDEX idx_auth_blocks_blocker ON auth.blocks (blocker_id);
CREATE INDEX idx_auth_blocks_blocked ON auth.blocks (blocked_id);

-- =============================================================================
-- Mute List
-- =============================================================================

CREATE TABLE auth.mutes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    muter_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    muted_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_mutes UNIQUE (muter_id, muted_id),
    CONSTRAINT chk_auth_mutes_not_self CHECK (muter_id != muted_id)
);

COMMENT ON TABLE auth.mutes IS 'User mute list';

CREATE INDEX idx_auth_mutes_muter ON auth.mutes (muter_id);
CREATE INDEX idx_auth_mutes_muted ON auth.mutes (muted_id);

-- =============================================================================
-- Password Reset Tokens
-- =============================================================================

CREATE TABLE auth.password_resets (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    token_hash TEXT NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    used_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_password_resets_token UNIQUE (token_hash)
);

COMMENT ON TABLE auth.password_resets IS 'Password reset tokens';

CREATE INDEX idx_auth_password_resets_user ON auth.password_resets (user_id);
CREATE INDEX idx_auth_password_resets_expires ON auth.password_resets (expires_at) WHERE used_at IS NULL;

-- =============================================================================
-- Email Verification Tokens
-- =============================================================================

CREATE TABLE auth.email_verifications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    email auth.email NOT NULL,
    token_hash TEXT NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    verified_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_email_verifications_token UNIQUE (token_hash)
);

COMMENT ON TABLE auth.email_verifications IS 'Email verification tokens';

CREATE INDEX idx_auth_email_verifications_user ON auth.email_verifications (user_id);

-- =============================================================================
-- User Scripts
-- =============================================================================

CREATE TABLE auth.user_scripts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    name public.nonempty_text NOT NULL,
    slug public.slug NOT NULL,
    description TEXT,

    source TEXT NOT NULL,
    config JSONB NOT NULL DEFAULT '{}',
    state JSONB NOT NULL DEFAULT '{}',

    is_enabled BOOLEAN NOT NULL DEFAULT true,

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_user_scripts_slug UNIQUE (user_id, slug)
);

COMMENT ON TABLE auth.user_scripts IS 'Per-user Rhai scripts';

CREATE INDEX idx_auth_user_scripts_user ON auth.user_scripts (user_id);
CREATE INDEX idx_auth_user_scripts_enabled ON auth.user_scripts (user_id, is_enabled) WHERE is_enabled = true;

-- =============================================================================
-- Server Staff (created here since it references auth.users)
-- =============================================================================

CREATE TABLE server.staff (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    role server.server_role NOT NULL,
    appointed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    appointed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_server_staff_user UNIQUE (user_id)
);

COMMENT ON TABLE server.staff IS 'Server-level administrative staff';

-- =============================================================================
-- Server Prop Library (Global Props)
-- =============================================================================

CREATE TABLE server.props (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    name public.nonempty_text NOT NULL,
    slug public.slug NOT NULL,
    description TEXT,
    tags TEXT[] NOT NULL DEFAULT '{}',

    asset_path public.asset_path NOT NULL,
    thumbnail_path public.asset_path,

    -- Default avatar positioning (content layer OR emotion layer, mutually exclusive)
    default_layer server.avatar_layer,
    default_emotion server.emotion_state,
    default_position SMALLINT CHECK (default_position IS NULL OR default_position BETWEEN 0 AND 8),

    -- Default scale factor for dropped props (10% - 1000%)
    default_scale REAL NOT NULL DEFAULT 1.0 CHECK (default_scale >= 0.1 AND default_scale <= 10.0),

    -- Optional JSON Schema for validating state structure
    state_schema JSONB,

    is_unique BOOLEAN NOT NULL DEFAULT false,
    is_transferable BOOLEAN NOT NULL DEFAULT true,
    is_portable BOOLEAN NOT NULL DEFAULT true,
    is_droppable BOOLEAN NOT NULL DEFAULT true,
    is_public BOOLEAN NOT NULL DEFAULT false,

    is_active BOOLEAN NOT NULL DEFAULT true,
    available_from TIMESTAMPTZ,
    available_until TIMESTAMPTZ,

    created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_server_props_slug UNIQUE (slug),
    CONSTRAINT chk_server_props_availability CHECK (
        available_from IS NULL OR available_until IS NULL OR available_from < available_until
    ),
    -- Props can be: non-avatar (all NULL), content layer, OR emotion layer (mutually exclusive)
    CONSTRAINT chk_server_props_positioning CHECK (
        -- Nothing set (non-avatar prop)
        (default_layer IS NULL AND default_emotion IS NULL AND default_position IS NULL) OR
        -- Content layer prop (skin/clothes/accessories at position 0-8)
        (default_layer IS NOT NULL AND default_emotion IS NULL AND default_position IS NOT NULL) OR
        -- Emotion layer prop (neutral/happy/sad/etc at position 0-8)
        (default_layer IS NULL AND default_emotion IS NOT NULL AND default_position IS NOT NULL)
    )
);

COMMENT ON TABLE server.props IS 'Global prop library (64x64 pixels, center-anchored)';
COMMENT ON COLUMN server.props.state_schema IS 'Optional JSON Schema defining valid state structure for this prop';

CREATE INDEX idx_server_props_tags ON server.props USING GIN (tags);
CREATE INDEX idx_server_props_active ON server.props (is_active) WHERE is_active = true;
CREATE INDEX idx_server_props_public ON server.props (is_public) WHERE is_public = true;

COMMENT ON COLUMN server.props.is_public IS
    'When true, prop appears in the public Server inventory tab. Uses filtered index idx_server_props_public.';

-- =============================================================================
-- Audio Library
-- =============================================================================

CREATE TABLE server.audio (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    name public.nonempty_text NOT NULL,
    slug public.slug NOT NULL,
    description TEXT,
    category server.audio_category NOT NULL,
    tags TEXT[] NOT NULL DEFAULT '{}',

    asset_path public.asset_path NOT NULL,
    duration_seconds REAL NOT NULL CHECK (duration_seconds > 0),
    is_loopable BOOLEAN NOT NULL DEFAULT false,

    is_active BOOLEAN NOT NULL DEFAULT true,

    created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_server_audio_slug UNIQUE (slug)
);

COMMENT ON TABLE server.audio IS 'Global audio library';

CREATE INDEX idx_server_audio_category ON server.audio (category);
CREATE INDEX idx_server_audio_tags ON server.audio USING GIN (tags);

-- =============================================================================
-- Reserved Names
-- =============================================================================

CREATE TABLE server.reserved_names (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    name TEXT NOT NULL,
    name_type server.reserved_name_type NOT NULL,
    reason TEXT,

    reserved_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

COMMENT ON TABLE server.reserved_names IS 'Names reserved from use by users';

CREATE UNIQUE INDEX uq_server_reserved_names ON server.reserved_names (lower(name), name_type);

-- =============================================================================
-- Server Scripts
-- =============================================================================

CREATE TABLE server.scripts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    name public.nonempty_text NOT NULL,
    slug public.slug NOT NULL,
    description TEXT,

    source TEXT NOT NULL,
    config JSONB NOT NULL DEFAULT '{}',
    state JSONB NOT NULL DEFAULT '{}',

    is_enabled BOOLEAN NOT NULL DEFAULT true,
    run_on_user_login BOOLEAN NOT NULL DEFAULT false,
    run_on_user_logout BOOLEAN NOT NULL DEFAULT false,
    run_on_registration BOOLEAN NOT NULL DEFAULT false,
    run_on_server_shutdown BOOLEAN NOT NULL DEFAULT false,

    created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_server_scripts_slug UNIQUE (slug)
);

COMMENT ON TABLE server.scripts IS 'Server-wide Rhai scripts';

CREATE INDEX idx_server_scripts_enabled ON server.scripts (is_enabled) WHERE is_enabled = true;

-- =============================================================================
-- User Inventory (moved from props.inventory)
-- =============================================================================
-- User inventory stores props owned by users (worn on avatar or in bag).
-- Inventory items reference a source: server prop, realm prop, or user upload.
-- The denormalized prop_name/prop_asset_path are cached at acquisition time.
-- =============================================================================

CREATE TABLE auth.inventory (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    -- Source of the prop (at least one required)
    server_prop_id UUID REFERENCES server.props(id) ON DELETE SET NULL,
    realm_prop_id UUID,  -- FK added in 030_realm.sql
    upload_id UUID,      -- Future: user uploads

    -- Denormalized display info (cached at acquisition)
    prop_name TEXT NOT NULL,
    prop_asset_path public.asset_path NOT NULL,
    layer server.avatar_layer,
    position SMALLINT CHECK (position IS NULL OR position BETWEEN 0 AND 8),

    -- Provenance chain for tracking history
    provenance JSONB NOT NULL DEFAULT '[]',
    origin server.prop_origin NOT NULL,

    -- Behavioral flags (cached from source at acquisition)
    is_transferable BOOLEAN NOT NULL DEFAULT true,
    is_portable BOOLEAN NOT NULL DEFAULT true,
    is_droppable BOOLEAN NOT NULL DEFAULT true,

    -- Public state columns (persist through transfer)
    server_state JSONB NOT NULL DEFAULT '{}',
    realm_state JSONB NOT NULL DEFAULT '{}',
    user_state JSONB NOT NULL DEFAULT '{}',

    -- Private state columns (cleared on transfer)
    server_private_state JSONB NOT NULL DEFAULT '{}',
    realm_private_state JSONB NOT NULL DEFAULT '{}',
    user_private_state JSONB NOT NULL DEFAULT '{}',

    acquired_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    -- At least one source must be present
    CONSTRAINT chk_auth_inventory_has_source CHECK (
        server_prop_id IS NOT NULL OR realm_prop_id IS NOT NULL OR upload_id IS NOT NULL
    )
);

COMMENT ON TABLE auth.inventory IS 'User-owned props (denormalized for performance)';
COMMENT ON COLUMN auth.inventory.provenance IS 'Array of {from_user, timestamp, method} objects';
COMMENT ON COLUMN auth.inventory.server_state IS 'Public state visible to everyone, persists through transfer';
COMMENT ON COLUMN auth.inventory.realm_state IS 'Public state visible to realm members, persists through transfer';
COMMENT ON COLUMN auth.inventory.user_state IS 'Public state visible to item inspectors, persists through transfer';
COMMENT ON COLUMN auth.inventory.server_private_state IS 'Private owner state, cleared on transfer';
COMMENT ON COLUMN auth.inventory.realm_private_state IS 'Private owner state, cleared on transfer';
COMMENT ON COLUMN auth.inventory.user_private_state IS 'Private owner state, cleared on transfer';
COMMENT ON COLUMN auth.inventory.updated_at IS 'Timestamp of last modification (auto-updated by trigger)';

CREATE INDEX idx_auth_inventory_user ON auth.inventory (user_id);
CREATE INDEX idx_auth_inventory_server_prop ON auth.inventory (server_prop_id)
    WHERE server_prop_id IS NOT NULL;
CREATE INDEX idx_auth_inventory_realm_prop ON auth.inventory (realm_prop_id)
    WHERE realm_prop_id IS NOT NULL;
CREATE INDEX idx_inventory_server_state ON auth.inventory USING GIN (server_state)
    WHERE server_state != '{}';
CREATE INDEX idx_inventory_realm_state ON auth.inventory USING GIN (realm_state)
    WHERE realm_state != '{}';

-- =============================================================================
-- User Avatars (moved from props.avatars)
-- =============================================================================
-- Avatar configurations per user (up to 10 slots: 0-9).
-- Uses 15 columns per layer, 135 total for the 9-position grid system.
-- =============================================================================

CREATE TABLE auth.avatars (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    slot_number SMALLINT NOT NULL CHECK (slot_number >= 0 AND slot_number <= 9),

    name public.display_name,
    is_default BOOLEAN NOT NULL DEFAULT false,
    last_emotion SMALLINT NOT NULL DEFAULT 0 CHECK (last_emotion >= 0 AND last_emotion <= 11),

    -- Content layers: skin (3), clothes (3), accessories (3) = 9 layers x 9 positions = 81 potential slots
    -- But we use 3 layers x 9 positions = 27 content slots
    l_skin_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_skin_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    l_clothes_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_clothes_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    l_accessories_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    l_accessories_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    -- Emotion layers: 12 emotions x 9 positions = 108 slots
    e_neutral_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_neutral_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_happy_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_happy_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_sad_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sad_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_angry_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_angry_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_surprised_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_surprised_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_thinking_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_thinking_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_laughing_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_laughing_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_crying_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_crying_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_love_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_love_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_confused_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_confused_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_sleeping_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_sleeping_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    e_wink_0 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_1 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_2 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_3 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_4 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_5 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_6 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_7 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,
    e_wink_8 UUID REFERENCES auth.inventory(id) ON DELETE SET NULL,

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_auth_avatars_slot UNIQUE (user_id, slot_number)
);

COMMENT ON TABLE auth.avatars IS 'User avatar configurations with 135 prop slots';

CREATE INDEX idx_auth_avatars_user ON auth.avatars (user_id);
CREATE INDEX idx_auth_avatars_default ON auth.avatars (user_id, is_default) WHERE is_default = true;

-- =============================================================================
-- Active Avatars (moved from props.active_avatars)
-- =============================================================================
-- Tracks which avatar a user is currently using in each realm.
-- =============================================================================

CREATE TABLE auth.active_avatars (
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    realm_id UUID NOT NULL,  -- FK added in 030_realm.sql after realm.realms exists
    avatar_id UUID REFERENCES auth.avatars(id) ON DELETE SET NULL,

    -- User-selected avatars from avatar stores (lower priority than custom avatar)
    selected_server_avatar_id UUID,  -- FK added in 025_server_avatars.sql
    selected_realm_avatar_id UUID,   -- FK added in 035_realm_avatars.sql

    current_emotion server.emotion_state NOT NULL DEFAULT 'happy',

    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    PRIMARY KEY (user_id, realm_id)
);

COMMENT ON TABLE auth.active_avatars IS 'Current avatar per user per realm';
COMMENT ON COLUMN auth.active_avatars.avatar_id IS
    'User custom avatar (highest priority, nullable for users without custom avatars)';
COMMENT ON COLUMN auth.active_avatars.selected_server_avatar_id IS
    'User-selected server avatar (from avatar store), lower priority than custom avatar';
COMMENT ON COLUMN auth.active_avatars.selected_realm_avatar_id IS
    'User-selected realm avatar (from avatar store), higher priority than server selection';

-- =============================================================================
-- Server-Level Moderation: IP Bans
-- =============================================================================

CREATE TABLE server.ip_bans (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    ip_address INET NOT NULL,
    ip_range CIDR,  -- Optional CIDR range for subnet bans

    reason TEXT NOT NULL,
    evidence JSONB NOT NULL DEFAULT '[]',

    banned_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    expires_at TIMESTAMPTZ,  -- NULL = permanent

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT uq_server_ip_bans_address UNIQUE (ip_address)
);

COMMENT ON TABLE server.ip_bans IS 'Server-wide IP address bans';

CREATE INDEX idx_server_ip_bans_range ON server.ip_bans USING GIST (ip_range inet_ops)
    WHERE ip_range IS NOT NULL;
CREATE INDEX idx_server_ip_bans_expires ON server.ip_bans (expires_at)
    WHERE expires_at IS NOT NULL;

-- =============================================================================
-- Server-Level Moderation: User Bans
-- =============================================================================

CREATE TABLE server.bans (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    reason TEXT NOT NULL,
    evidence JSONB NOT NULL DEFAULT '[]',

    banned_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    expires_at TIMESTAMPTZ,  -- NULL = permanent

    is_active BOOLEAN NOT NULL DEFAULT true,
    unbanned_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    unbanned_at TIMESTAMPTZ,
    unban_reason TEXT,

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

COMMENT ON TABLE server.bans IS 'Server-wide user bans';

CREATE INDEX idx_server_bans_user ON server.bans (user_id);
CREATE INDEX idx_server_bans_active ON server.bans (user_id, is_active) WHERE is_active = true;
CREATE INDEX idx_server_bans_expires ON server.bans (expires_at) WHERE expires_at IS NOT NULL AND is_active = true;

-- =============================================================================
-- Server-Level Moderation: Server Mutes
-- =============================================================================

CREATE TABLE server.mutes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    reason TEXT NOT NULL,

    muted_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    expires_at TIMESTAMPTZ,  -- NULL = permanent

    is_active BOOLEAN NOT NULL DEFAULT true,
    unmuted_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    unmuted_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

COMMENT ON TABLE server.mutes IS 'Server-wide user mutes (cannot send messages anywhere)';

CREATE INDEX idx_server_mutes_user ON server.mutes (user_id);
CREATE INDEX idx_server_mutes_active ON server.mutes (user_id, is_active) WHERE is_active = true;

-- =============================================================================
-- Server-Level Moderation: Content Filters
-- =============================================================================

CREATE TABLE server.content_filters (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    pattern TEXT NOT NULL,
    is_regex BOOLEAN NOT NULL DEFAULT false,
    is_case_sensitive BOOLEAN NOT NULL DEFAULT false,

    action server.filter_action NOT NULL DEFAULT 'block',
    replacement TEXT,

    reason TEXT,

    is_active BOOLEAN NOT NULL DEFAULT true,

    created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

COMMENT ON TABLE server.content_filters IS 'Server-wide content filtering rules';

CREATE INDEX idx_server_content_filters_active ON server.content_filters (is_active) WHERE is_active = true;

-- =============================================================================
-- Server-Level Moderation: Action Log
-- =============================================================================

CREATE TABLE server.moderation_actions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    action_type server.action_type NOT NULL,

    target_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    moderator_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,

    reason TEXT NOT NULL,
    evidence JSONB NOT NULL DEFAULT '[]',
    metadata JSONB NOT NULL DEFAULT '{}',

    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

COMMENT ON TABLE server.moderation_actions IS 'Log of server-level moderation actions';

CREATE INDEX idx_server_moderation_actions_target ON server.moderation_actions (target_user_id);
CREATE INDEX idx_server_moderation_actions_moderator ON server.moderation_actions (moderator_id);
CREATE INDEX idx_server_moderation_actions_type ON server.moderation_actions (action_type);
CREATE INDEX idx_server_moderation_actions_created ON server.moderation_actions (created_at DESC);

COMMIT;
