-- ==========================================
-- Chat Switchboard — v0.16.0 Consolidated Schema
-- ==========================================
-- Clean-slate schema. Replaces all 001–009 migrations.
-- Drop DB and re-create before applying.
--
-- Changes from v0.15.1 (9-file) schema:
--   • Folded all incremental migrations into base CREATEs
--   • Dropped api_key_plain (vault backfill complete)
--   • Dropped projects, project_channels (unused, v0.19.0 redesigns)
--   • Dropped 'moderator' from users.role CHECK (dead code)
--   • CI usernames from the start (LOWER() unique indexes)
--   • Added: groups, group_members, resource_grants (v0.16.0)
--   • Added: pgvector extension declaration
--
-- Design principles:
--   • Explicit scope enums over nullable column tri-states
--   • Personas as trust boundaries with extensible grants
--   • Groups as pure access-control lists (decouple from teams)
--   • Secure by default (models hidden until admin enables)
--   • Only tables with active handlers — no placeholder tables
-- ==========================================

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

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

-- ── Upgrade-path cleanup ───────────────────
-- These objects existed in the 001–009 migration chain but were removed
-- in the v0.16.0 consolidation. Safe no-ops on fresh installs.

DROP TABLE IF EXISTS project_channels CASCADE;
DROP TABLE IF EXISTS projects CASCADE;

DO $$ BEGIN
    IF EXISTS (
        SELECT 1 FROM information_schema.columns
        WHERE table_name = 'provider_configs' AND column_name = 'api_key_plain'
    ) THEN
        ALTER TABLE provider_configs DROP COLUMN api_key_plain;
    END IF;
END $$;

-- ── 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;


-- =========================================
-- 1. 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 NOT NULL,
    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,

    -- 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 (no bare UNIQUE constraint)
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 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.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';


-- =========================================
-- 2. AUTH
-- =========================================

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;


-- =========================================
-- 3. TEAMS
-- =========================================

CREATE TABLE IF NOT EXISTS teams (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name        VARCHAR(200) NOT NULL UNIQUE,
    description TEXT DEFAULT '',
    created_by  UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    is_active   BOOLEAN DEFAULT true,
    settings    JSONB DEFAULT '{}'::jsonb,
    created_at  TIMESTAMPTZ DEFAULT NOW(),
    updated_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active) WHERE is_active = true;
DROP TRIGGER IF EXISTS teams_updated_at ON teams;
CREATE TRIGGER teams_updated_at BEFORE UPDATE ON teams
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

COMMENT ON COLUMN teams.settings IS 'Team policies: {"require_private_providers": false}';

CREATE TABLE IF NOT EXISTS team_members (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    team_id     UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role        VARCHAR(20) NOT NULL DEFAULT 'member'
                CHECK (role IN ('admin', 'member')),
    joined_at   TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(team_id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);


-- =========================================
-- 4. GROUPS (v0.16.0)
-- =========================================
-- Pure access-control lists. Decouple resource visibility from team membership.
-- Teams = organizational structure. Roles = vertical permissions.
-- Groups = who can access what (cross-cutting).

CREATE TABLE IF NOT EXISTS groups (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name        VARCHAR(100) NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    scope       VARCHAR(20) NOT NULL DEFAULT 'global'
                CHECK (scope IN ('global', 'team')),
    team_id     UUID REFERENCES teams(id) ON DELETE CASCADE,
    created_by  UUID NOT NULL REFERENCES users(id),
    created_at  TIMESTAMPTZ DEFAULT NOW(),
    updated_at  TIMESTAMPTZ DEFAULT NOW(),

    -- Global groups: team_id must be NULL
    -- Team groups: team_id must be set
    CONSTRAINT groups_scope_team CHECK (
        (scope = 'global' AND team_id IS NULL) OR
        (scope = 'team' AND team_id IS NOT NULL)
    )
);

-- Unique name within scope (global names globally unique,
-- team names unique within team)
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
    ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000'));

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

COMMENT ON TABLE groups IS 'Access-control groups. Decouple resource visibility from team membership.';
COMMENT ON COLUMN groups.scope IS 'global: admin-managed, can span teams. team: team-admin-managed, team-internal.';

CREATE TABLE IF NOT EXISTS group_members (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    group_id    UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    added_by    UUID NOT NULL REFERENCES users(id),
    added_at    TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(group_id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);


-- =========================================
-- 5. PROVIDER CONFIGS
-- =========================================
-- scope='global': admin-managed, visible to all users (owner_id IS NULL)
-- scope='team':   team admin-managed (owner_id = teams.id)
-- scope='personal': user's own keys (owner_id = users.id)

CREATE TABLE IF NOT EXISTS provider_configs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    scope           VARCHAR(10) NOT NULL
                    CHECK (scope IN ('global', 'team', 'personal')),
    owner_id        UUID,
    name            VARCHAR(100) NOT NULL,
    provider        VARCHAR(50) NOT NULL,
    endpoint        TEXT NOT NULL,

    -- Encrypted API key (AES-256-GCM)
    api_key_enc     BYTEA,
    key_nonce       BYTEA,
    key_scope       TEXT NOT NULL DEFAULT 'global',

    model_default   VARCHAR(100),
    config          JSONB DEFAULT '{}'::jsonb,
    headers         JSONB DEFAULT '{}'::jsonb,
    settings        JSONB DEFAULT '{}'::jsonb,
    is_active       BOOLEAN DEFAULT true,
    is_private      BOOLEAN DEFAULT false,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope);
CREATE INDEX IF NOT EXISTS idx_provider_configs_owner ON provider_configs(owner_id) WHERE owner_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active) WHERE is_active = true;
DROP TRIGGER IF EXISTS provider_configs_updated_at ON provider_configs;
CREATE TRIGGER provider_configs_updated_at BEFORE UPDATE ON provider_configs
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

COMMENT ON COLUMN provider_configs.scope IS 'global=admin-managed, team=team-scoped, personal=user BYOK';
COMMENT ON COLUMN provider_configs.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal scope';
COMMENT ON COLUMN provider_configs.api_key_enc IS 'AES-256-GCM encrypted API key (BYTEA)';
COMMENT ON COLUMN provider_configs.key_nonce IS 'AES-GCM nonce for api_key_enc';
COMMENT ON COLUMN provider_configs.key_scope IS 'Encryption tier: global/team use env key, personal uses UEK';
COMMENT ON COLUMN provider_configs.headers IS 'Custom HTTP headers (e.g. OpenRouter HTTP-Referer)';
COMMENT ON COLUMN provider_configs.settings IS 'Provider-specific params (e.g. Venice web_search, OpenRouter route)';
COMMENT ON COLUMN provider_configs.is_private IS 'Data stays on-prem (local/self-hosted provider)';


-- =========================================
-- 6. MODEL CATALOG
-- =========================================
-- Intrinsic capabilities for models the system knows about.
-- Disabled by default — admin must enable.

CREATE TABLE IF NOT EXISTS model_catalog (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    provider_config_id  UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
    model_id            TEXT NOT NULL,
    display_name        TEXT,
    model_type          VARCHAR(20) DEFAULT 'chat',
    capabilities        JSONB NOT NULL DEFAULT '{}',
    pricing             JSONB,
    visibility          VARCHAR(10) DEFAULT 'disabled'
                        CHECK (visibility IN ('enabled', 'disabled', 'team')),
    last_synced_at      TIMESTAMPTZ,
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    updated_at          TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(provider_config_id, model_id)
);

CREATE INDEX IF NOT EXISTS idx_model_catalog_provider ON model_catalog(provider_config_id);
CREATE INDEX IF NOT EXISTS idx_model_catalog_enabled ON model_catalog(visibility) WHERE visibility = 'enabled';
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
DROP TRIGGER IF EXISTS model_catalog_updated_at ON model_catalog;
CREATE TRIGGER model_catalog_updated_at BEFORE UPDATE ON model_catalog
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

COMMENT ON COLUMN model_catalog.visibility IS 'disabled by default — admin must enable for global models';
COMMENT ON COLUMN model_catalog.model_type IS 'chat, embedding, image — from provider API sync';
COMMENT ON COLUMN model_catalog.capabilities IS 'Intrinsic: {"streaming","tool_calling","vision","thinking","reasoning","code_optimized","web_search","max_context","max_output_tokens"}';


-- =========================================
-- 7. PERSONAS
-- =========================================

CREATE TABLE IF NOT EXISTS personas (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name                VARCHAR(100) NOT NULL,
    description         TEXT DEFAULT '',
    icon                VARCHAR(10) DEFAULT '',
    avatar              TEXT DEFAULT '',

    -- Base model binding
    base_model_id       TEXT NOT NULL,
    provider_config_id  UUID REFERENCES provider_configs(id) ON DELETE SET NULL,

    -- Behavioral configuration
    system_prompt       TEXT DEFAULT '',
    temperature         FLOAT,
    max_tokens          INT,
    thinking_budget     INT,
    top_p               FLOAT,

    -- Scope & ownership
    scope               VARCHAR(10) NOT NULL
                        CHECK (scope IN ('global', 'team', 'personal')),
    owner_id            UUID,
    created_by          UUID NOT NULL REFERENCES users(id),

    -- State
    is_active           BOOLEAN DEFAULT true,
    is_shared           BOOLEAN DEFAULT false,

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

CREATE INDEX IF NOT EXISTS idx_personas_scope ON personas(scope);
CREATE INDEX IF NOT EXISTS idx_personas_owner ON personas(owner_id) WHERE owner_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active) WHERE is_active = true;
DROP TRIGGER IF EXISTS personas_updated_at ON personas;
CREATE TRIGGER personas_updated_at BEFORE UPDATE ON personas
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

COMMENT ON COLUMN personas.scope IS 'global=all users, team=team members, personal=creator only';
COMMENT ON COLUMN personas.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal';
COMMENT ON COLUMN personas.is_shared IS 'Personal Personas shared with others (read-only)';


-- =========================================
-- 8. PERSONA GRANTS (what a Persona can do)
-- =========================================
-- Controls tools, KBs, and API endpoints that a Persona has access TO.
-- NOT to be confused with resource_grants (who can USE a resource).

CREATE TABLE IF NOT EXISTS persona_grants (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    persona_id      UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
    grant_type      VARCHAR(30) NOT NULL,
    grant_ref       TEXT NOT NULL,
    config          JSONB DEFAULT '{}'::jsonb,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(persona_id, grant_type, grant_ref)
);

CREATE INDEX IF NOT EXISTS idx_persona_grants_persona ON persona_grants(persona_id);
CREATE INDEX IF NOT EXISTS idx_persona_grants_type ON persona_grants(grant_type);

COMMENT ON TABLE persona_grants IS 'What a Persona can do: tools it can call, KBs it can search, etc.';
COMMENT ON COLUMN persona_grants.grant_type IS 'Extensible: tool, knowledge_base, api_endpoint';
COMMENT ON COLUMN persona_grants.grant_ref IS 'tool: function name. knowledge_base: UUID. api_endpoint: identifier.';
COMMENT ON COLUMN persona_grants.config IS 'Type-specific config, e.g. {"read_only": true} for KB grants';


-- =========================================
-- 9. RESOURCE GRANTS (v0.16.0 — who can USE a resource)
-- =========================================
-- Controls which users (via groups) can access a resource.
-- NOT to be confused with persona_grants (what a Persona can do).
--
-- persona_grants:   "CodeBot can use calculator"      (Persona → tool)
-- resource_grants:  "Engineering can use CodeBot"      (group → Persona)

CREATE TABLE IF NOT EXISTS resource_grants (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    resource_type   VARCHAR(30) NOT NULL
                    CHECK (resource_type IN ('persona', 'knowledge_base')),
    resource_id     UUID NOT NULL,
    grant_scope     VARCHAR(20) NOT NULL DEFAULT 'team_only'
                    CHECK (grant_scope IN ('team_only', 'global', 'groups')),
    granted_groups  UUID[] NOT NULL DEFAULT '{}',
    created_by      UUID NOT NULL REFERENCES users(id),
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),

    -- One grant row per resource
    UNIQUE(resource_type, resource_id)
);

CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
    ON resource_grants(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_resource_grants_groups
    ON resource_grants USING gin(granted_groups);

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

COMMENT ON TABLE resource_grants IS 'Who can USE a resource (personas, KBs). Cross-team access via groups.';
COMMENT ON COLUMN resource_grants.grant_scope IS 'team_only: existing team behavior. global: all users. groups: specific group list.';
COMMENT ON COLUMN resource_grants.granted_groups IS 'UUID array of group IDs. Only meaningful when grant_scope = groups.';


-- =========================================
-- 10. 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()
);

-- Secure by default
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')
ON CONFLICT (key) DO NOTHING;

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


-- =========================================
-- 11. GLOBAL SETTINGS (non-policy config)
-- =========================================

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": "",
        "position": "both",
        "bg": "#007a33",
        "fg": "#ffffff"
    }'::jsonb),
    ('banner_presets', '{
        "development":  { "text": "DEVELOPMENT",  "bg": "#007a33", "fg": "#ffffff" },
        "testing":      { "text": "TESTING",       "bg": "#502b85", "fg": "#ffffff" },
        "staging":      { "text": "STAGING",       "bg": "#0033a0", "fg": "#ffffff" },
        "production":   { "text": "PRODUCTION",    "bg": "#c8102e", "fg": "#ffffff" },
        "training":     { "text": "TRAINING",      "bg": "#ff8c00", "fg": "#000000" },
        "demo":         { "text": "DEMO",          "bg": "#fce83a", "fg": "#000000" }
    }'::jsonb),
    ('model_roles', '{
        "utility":    { "primary": null, "fallback": null },
        "embedding":  { "primary": null, "fallback": null },
        "generation": { "primary": null, "fallback": null }
    }'::jsonb)
ON CONFLICT (key) DO NOTHING;


-- =========================================
-- 12. USER MODEL SETTINGS
-- =========================================

CREATE TABLE IF NOT EXISTS user_model_settings (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    model_id        TEXT NOT NULL,
    hidden          BOOLEAN DEFAULT false,
    preferred_temperature   FLOAT,
    preferred_max_tokens    INT,
    sort_order      INT DEFAULT 0,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(user_id, model_id)
);

CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id);
DROP TRIGGER IF EXISTS user_model_settings_updated_at ON user_model_settings;
CREATE TRIGGER user_model_settings_updated_at BEFORE UPDATE ON user_model_settings
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();


-- =========================================
-- 13. CHANNELS
-- =========================================

CREATE TABLE IF NOT EXISTS channels (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID REFERENCES users(id) ON DELETE CASCADE,
    title           VARCHAR(500) NOT NULL,
    description     TEXT,
    type            VARCHAR(20) DEFAULT 'direct'
                    CHECK (type IN ('direct', 'group', 'channel')),
    model           VARCHAR(100),
    system_prompt   TEXT,
    provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
    is_archived     BOOLEAN DEFAULT false,
    is_pinned       BOOLEAN DEFAULT false,
    folder_id       UUID,   -- FK added after folders table created
    folder          TEXT,   -- simple text folder name (frontend-managed)
    team_id         UUID REFERENCES teams(id) ON DELETE SET NULL,
    settings        JSONB DEFAULT '{}'::jsonb,
    tags            TEXT[],
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_channels_user ON channels(user_id);
CREATE INDEX IF NOT EXISTS idx_channels_updated ON channels(updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channels_tags ON channels USING GIN(tags);
DROP TRIGGER IF EXISTS channels_updated_at ON channels;
CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

COMMENT ON COLUMN channels.type IS 'direct=1:1 AI chat, group=multi-model, channel=named persistent';


-- =========================================
-- 14. MESSAGES (with tree/forking support)
-- =========================================

CREATE TABLE IF NOT EXISTS messages (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    channel_id      UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    role            VARCHAR(20) NOT NULL
                    CHECK (role IN ('user', 'assistant', 'system', 'tool')),
    content         TEXT NOT NULL,
    model           VARCHAR(100),
    tokens_used     INTEGER,
    tool_calls      JSONB,
    metadata        JSONB DEFAULT '{}'::jsonb,
    parent_id       UUID REFERENCES messages(id) ON DELETE SET NULL,
    sibling_index   INTEGER DEFAULT 0,
    participant_type VARCHAR(10) DEFAULT 'user',
    participant_id  VARCHAR(255),
    deleted_at      TIMESTAMPTZ,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at);
CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id);
CREATE INDEX IF NOT EXISTS idx_messages_alive ON messages(channel_id, created_at)
    WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_messages_parent_alive ON messages(parent_id, sibling_index)
    WHERE deleted_at IS NULL;

COMMENT ON COLUMN messages.parent_id IS 'Tree parent for conversation forking';
COMMENT ON COLUMN messages.sibling_index IS 'Position among siblings (0-indexed)';
COMMENT ON COLUMN messages.participant_type IS 'user or model';
COMMENT ON COLUMN messages.participant_id IS 'user UUID or model identifier string';


-- =========================================
-- 15. CHANNEL MEMBERS & MODELS
-- =========================================

CREATE TABLE IF NOT EXISTS channel_members (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    channel_id      UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    user_id         UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role            VARCHAR(20) DEFAULT 'member',
    joined_at       TIMESTAMPTZ DEFAULT NOW(),
    last_read_at    TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(channel_id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_channel_members_channel ON channel_members(channel_id);
CREATE INDEX IF NOT EXISTS idx_channel_members_user ON channel_members(user_id);

CREATE TABLE IF NOT EXISTS channel_models (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    channel_id      UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    model_id        VARCHAR(255) NOT NULL,
    provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
    display_name    VARCHAR(100),
    system_prompt   TEXT,
    settings        JSONB DEFAULT '{}'::jsonb,
    is_default      BOOLEAN DEFAULT false,
    added_at        TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(channel_id, model_id)
);

CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id);


-- =========================================
-- 16. CHANNEL CURSORS (forking navigation)
-- =========================================

CREATE TABLE IF NOT EXISTS channel_cursors (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    channel_id      UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    user_id         UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    active_leaf_id  UUID REFERENCES messages(id) ON DELETE SET NULL,
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(channel_id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_channel_cursors_channel ON channel_cursors(channel_id);
CREATE INDEX IF NOT EXISTS idx_channel_cursors_user ON channel_cursors(user_id);


-- =========================================
-- 17. FOLDERS
-- =========================================
-- Channel folders. Note: projects table deferred to v0.19.0.

CREATE TABLE IF NOT EXISTS folders (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    name            VARCHAR(200) NOT NULL,
    parent_id       UUID REFERENCES folders(id) ON DELETE CASCADE,
    sort_order      INT DEFAULT 0,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(user_id, name, parent_id)
);

CREATE INDEX IF NOT EXISTS idx_folders_user ON folders(user_id);
CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id);
DROP TRIGGER IF EXISTS folders_updated_at ON folders;
CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- Deferred FK: channels.folder_id → folders.id
DO $$ BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_folder'
    ) THEN
        ALTER TABLE channels ADD CONSTRAINT fk_channels_folder
            FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL;
    END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;


-- =========================================
-- 18. NOTES
-- =========================================

CREATE TABLE IF NOT EXISTS notes (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id             UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title               VARCHAR(500) NOT NULL,
    content             TEXT DEFAULT '',
    folder_path         TEXT DEFAULT '/',
    tags                TEXT[] DEFAULT '{}',
    metadata            JSONB DEFAULT '{}',
    source_channel_id   UUID REFERENCES channels(id) ON DELETE SET NULL,
    team_id             UUID REFERENCES teams(id) ON DELETE SET NULL,
    search_vector       TSVECTOR,
    embedding           VECTOR(3072),
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    updated_at          TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
CREATE INDEX IF NOT EXISTS idx_notes_search ON notes USING GIN(search_vector);
CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(user_id, folder_path);
CREATE INDEX IF NOT EXISTS idx_notes_tags ON notes USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(user_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id) WHERE team_id IS NOT NULL;

CREATE OR REPLACE FUNCTION notes_search_update_fn()
RETURNS TRIGGER AS $$
BEGIN
    NEW.search_vector :=
        setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.title, '')), 'A') ||
        setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.content, '')), 'B');
    NEW.updated_at := NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS notes_search_update ON notes;
CREATE TRIGGER notes_search_update
    BEFORE INSERT OR UPDATE OF title, content ON notes
    FOR EACH ROW EXECUTE FUNCTION notes_search_update_fn();


-- =========================================
-- 19. AUDIT LOG
-- =========================================

CREATE TABLE IF NOT EXISTS audit_log (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    actor_id        UUID REFERENCES users(id) ON DELETE SET NULL,
    action          VARCHAR(100) NOT NULL,
    resource_type   VARCHAR(50) NOT NULL,
    resource_id     VARCHAR(255),
    metadata        JSONB DEFAULT '{}'::jsonb,
    ip_address      VARCHAR(45),
    user_agent      TEXT DEFAULT '',
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id) WHERE actor_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);

COMMENT ON TABLE audit_log IS 'Immutable audit trail of all mutating operations';


-- =========================================
-- 20. USAGE TRACKING
-- =========================================

CREATE TABLE IF NOT EXISTS usage_log (
    id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    channel_id            UUID REFERENCES channels(id) ON DELETE SET NULL,
    user_id               UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    provider_config_id    UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
    provider_scope        TEXT NOT NULL DEFAULT 'global',
    model_id              TEXT NOT NULL,
    role                  TEXT,  -- NULL = user chat, 'utility', 'embedding', 'generation'
    prompt_tokens         INT NOT NULL DEFAULT 0,
    completion_tokens     INT NOT NULL DEFAULT 0,
    cache_creation_tokens INT NOT NULL DEFAULT 0,
    cache_read_tokens     INT NOT NULL DEFAULT 0,
    cost_input            NUMERIC(12,6),
    cost_output           NUMERIC(12,6),
    created_at            TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_usage_log_user     ON usage_log(user_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_created  ON usage_log(created_at);
CREATE INDEX IF NOT EXISTS idx_usage_log_provider ON usage_log(provider_config_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_model    ON usage_log(model_id);
CREATE INDEX IF NOT EXISTS idx_usage_log_scope    ON usage_log(provider_scope);


-- =========================================
-- 21. MODEL PRICING
-- =========================================

CREATE TABLE IF NOT EXISTS model_pricing (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    provider_config_id  UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
    model_id            TEXT NOT NULL,
    input_per_m         NUMERIC(10,6),
    output_per_m        NUMERIC(10,6),
    cache_create_per_m  NUMERIC(10,6),
    cache_read_per_m    NUMERIC(10,6),
    currency            TEXT NOT NULL DEFAULT 'USD',
    source              TEXT NOT NULL DEFAULT 'manual',
    updated_at          TIMESTAMPTZ DEFAULT NOW(),
    updated_by          UUID REFERENCES users(id),
    UNIQUE(provider_config_id, model_id)
);


-- =========================================
-- 22. EXTENSIONS
-- =========================================

CREATE TABLE IF NOT EXISTS extensions (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ext_id      VARCHAR(100) NOT NULL UNIQUE,
    name        VARCHAR(200) NOT NULL,
    version     VARCHAR(50)  NOT NULL DEFAULT '0.0.0',
    tier        VARCHAR(20)  NOT NULL DEFAULT 'browser',
    description TEXT         NOT NULL DEFAULT '',
    author      VARCHAR(200) NOT NULL DEFAULT '',
    manifest    JSONB        NOT NULL DEFAULT '{}',
    is_system   BOOLEAN      NOT NULL DEFAULT false,
    is_enabled  BOOLEAN      NOT NULL DEFAULT true,
    scope       VARCHAR(20)  NOT NULL DEFAULT 'global',
    team_id     UUID         REFERENCES teams(id) ON DELETE CASCADE,
    installed_by UUID        REFERENCES users(id) ON DELETE SET NULL,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_extensions_tier ON extensions(tier);
CREATE INDEX IF NOT EXISTS idx_extensions_enabled ON extensions(is_enabled) WHERE is_enabled = true;

CREATE TABLE IF NOT EXISTS extension_user_settings (
    extension_id UUID NOT NULL REFERENCES extensions(id) ON DELETE CASCADE,
    user_id      UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    settings     JSONB   NOT NULL DEFAULT '{}',
    is_enabled   BOOLEAN NOT NULL DEFAULT true,
    PRIMARY KEY (extension_id, user_id)
);


-- =========================================
-- 23. ATTACHMENTS
-- =========================================

CREATE TABLE IF NOT EXISTS attachments (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    channel_id      UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    user_id         UUID NOT NULL REFERENCES users(id),
    message_id      UUID REFERENCES messages(id) ON DELETE SET NULL,
    filename        VARCHAR(255) NOT NULL,
    content_type    VARCHAR(127) NOT NULL,
    size_bytes      BIGINT NOT NULL,
    storage_key     TEXT NOT NULL,
    extracted_text  TEXT,
    metadata        JSONB DEFAULT '{}'::jsonb,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_attachments_channel ON attachments(channel_id);
CREATE INDEX IF NOT EXISTS idx_attachments_user_size ON attachments(user_id);
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id) WHERE message_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_attachments_orphan ON attachments(created_at) WHERE message_id IS NULL;


-- =========================================
-- 24. KNOWLEDGE BASES
-- =========================================

CREATE TABLE IF NOT EXISTS knowledge_bases (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name            TEXT NOT NULL,
    description     TEXT NOT NULL DEFAULT '',
    scope           TEXT NOT NULL DEFAULT 'global',
    owner_id        UUID REFERENCES users(id) ON DELETE CASCADE,
    team_id         UUID REFERENCES teams(id) ON DELETE CASCADE,
    embedding_config JSONB NOT NULL DEFAULT '{}',
    document_count  INT NOT NULL DEFAULT 0,
    chunk_count     INT NOT NULL DEFAULT 0,
    total_bytes     BIGINT NOT NULL DEFAULT 0,
    status          TEXT NOT NULL DEFAULT 'active',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),

    CONSTRAINT kb_scope_check CHECK (
        (scope = 'global'   AND owner_id IS NULL) OR
        (scope = 'team'     AND team_id IS NOT NULL) OR
        (scope = 'personal' AND owner_id IS NOT NULL)
    )
);

CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope);
CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id) WHERE owner_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_kb_team  ON knowledge_bases(team_id)  WHERE team_id IS NOT NULL;

CREATE TABLE IF NOT EXISTS kb_documents (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    kb_id           UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
    filename        TEXT NOT NULL,
    content_type    TEXT NOT NULL,
    size_bytes      BIGINT NOT NULL,
    storage_key     TEXT NOT NULL,
    extracted_text  TEXT,
    chunk_count     INT NOT NULL DEFAULT 0,
    status          TEXT NOT NULL DEFAULT 'pending',
    error           TEXT,
    uploaded_by     UUID NOT NULL REFERENCES users(id),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id);

CREATE TABLE IF NOT EXISTS kb_chunks (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    kb_id           UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
    document_id     UUID NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE,
    chunk_index     INT NOT NULL,
    content         TEXT NOT NULL,
    token_count     INT NOT NULL DEFAULT 0,
    embedding       VECTOR(3072),
    metadata        JSONB NOT NULL DEFAULT '{}',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_kbchunk_kb  ON kb_chunks(kb_id);
CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id);

-- NOTE: IVFFlat index for similarity search created after first data load
-- (needs rows to train). The ingest handler creates it.

CREATE TABLE IF NOT EXISTS channel_knowledge_bases (
    channel_id      UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    kb_id           UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
    enabled         BOOLEAN NOT NULL DEFAULT true,
    added_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (channel_id, kb_id)
);
