-- ==========================================
-- Chat Switchboard — 001 Core
-- ==========================================
-- Users (with auth fields), refresh tokens, platform policies,
-- global settings, user presence.
-- Consolidated v0.28.0: merges 001 + 018 (auth_abstraction) + 016 (presence).
-- ==========================================

-- ── Extensions ──────────────────────────────

CREATE EXTENSION IF NOT EXISTS "pgcrypto";     -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "vector";       -- pgvector (KB embeddings)

-- ── Utility: auto-update updated_at ─────────

CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;


-- =========================================
-- USERS
-- =========================================

CREATE TABLE IF NOT EXISTS users (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username        VARCHAR(50) NOT NULL,
    email           VARCHAR(255) NOT NULL,
    password_hash   TEXT,                       -- nullable for mTLS/OIDC
    display_name    VARCHAR(100),
    avatar_url      TEXT,
    role            VARCHAR(20) DEFAULT 'user'
                    CHECK (role IN ('user', 'admin')),
    is_active       BOOLEAN DEFAULT true,
    settings        JSONB DEFAULT '{}'::jsonb,

    -- Auth abstraction (v0.24.0)
    auth_source     VARCHAR(20) NOT NULL DEFAULT 'builtin'
                    CHECK (auth_source IN ('builtin', 'mtls', 'oidc')),
    external_id     TEXT,
    handle          VARCHAR(100) NOT NULL,

    -- Vault: per-user encryption key (UEK) for BYOK API keys
    encrypted_uek   BYTEA,
    uek_salt        BYTEA,
    uek_nonce       BYTEA,
    vault_set       BOOLEAN NOT NULL DEFAULT false,

    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    last_login_at   TIMESTAMPTZ
);

-- Case-insensitive unique indexes
CREATE UNIQUE INDEX IF NOT EXISTS users_username_ci ON users (LOWER(username));
CREATE UNIQUE INDEX IF NOT EXISTS users_email_ci ON users (LOWER(email));
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(LOWER(handle));
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_external_id
    ON users(auth_source, external_id) WHERE external_id IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

DROP TRIGGER IF EXISTS users_updated_at ON users;
CREATE TRIGGER users_updated_at BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

COMMENT ON COLUMN users.auth_source IS 'builtin=password, mtls=cert, oidc=SSO';
COMMENT ON COLUMN users.external_id IS 'IdP subject claim (OIDC) or cert fingerprint (mTLS)';
COMMENT ON COLUMN users.handle IS 'Unique @mention handle, auto-generated from username';
COMMENT ON COLUMN users.encrypted_uek IS 'UEK encrypted with Argon2id(password)-derived key';
COMMENT ON COLUMN users.uek_salt      IS 'Argon2id salt for UEK derivation';
COMMENT ON COLUMN users.uek_nonce     IS 'AES-GCM nonce for UEK wrapping';
COMMENT ON COLUMN users.vault_set     IS 'true once UEK has been generated and wrapped';


-- =========================================
-- AUTH — REFRESH TOKENS
-- =========================================

CREATE TABLE IF NOT EXISTS refresh_tokens (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash  TEXT NOT NULL UNIQUE,
    expires_at  TIMESTAMPTZ NOT NULL,
    created_at  TIMESTAMPTZ DEFAULT NOW(),
    revoked_at  TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash) WHERE revoked_at IS NULL;


-- =========================================
-- PLATFORM POLICIES
-- =========================================

CREATE TABLE IF NOT EXISTS platform_policies (
    key         VARCHAR(50) PRIMARY KEY,
    value       TEXT NOT NULL,
    updated_by  UUID REFERENCES users(id),
    updated_at  TIMESTAMPTZ DEFAULT NOW()
);

INSERT INTO platform_policies (key, value) VALUES
    ('allow_user_byok',         'false'),
    ('allow_user_personas',     'false'),
    ('allow_raw_model_access',  'true'),
    ('allow_registration',      'true'),
    ('default_user_active',     'false'),
    ('allow_team_providers',    'true'),
    ('kb_direct_access',        'true')
ON CONFLICT (key) DO NOTHING;

COMMENT ON TABLE platform_policies IS 'Global admin switches controlling platform behavior';


-- =========================================
-- GLOBAL SETTINGS
-- =========================================

CREATE TABLE IF NOT EXISTS global_settings (
    key         VARCHAR(100) PRIMARY KEY,
    value       JSONB NOT NULL DEFAULT '{}'::jsonb,
    updated_at  TIMESTAMPTZ DEFAULT NOW(),
    updated_by  UUID REFERENCES users(id)
);

INSERT INTO global_settings (key, value) VALUES
    ('registration', '{"enabled": true}'::jsonb),
    ('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'::jsonb),
    ('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb),
    ('model_roles', '{
        "utility":    { "primary": null, "fallback": null },
        "embedding":  { "primary": null, "fallback": null },
        "generation": { "primary": null, "fallback": null }
    }'::jsonb),
    ('retention_ttl_days', '{"value": 0}'::jsonb)
ON CONFLICT (key) DO NOTHING;


-- =========================================
-- USER PRESENCE (v0.23.1)
-- =========================================

CREATE TABLE IF NOT EXISTS user_presence (
    user_id     UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
    last_seen   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    status      VARCHAR(20) NOT NULL DEFAULT 'online'
                CHECK (status IN ('online', 'away', 'offline'))
);

CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC);

COMMENT ON TABLE user_presence IS 'Heartbeat-driven presence. Rows upserted every 30s by active clients.';
