-- ==========================================
-- Chat Switchboard — v0.9.0 Consolidated Schema
-- ==========================================
-- Clean-slate schema. Replaces all 001–021 migrations.
-- Drop DB and re-create before applying.
--
-- Design principles:
--   • Explicit scope enums over nullable column tri-states
--   • Personas as trust boundaries with extensible grants
--   • 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()

-- ── 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) UNIQUE NOT NULL,
    email           VARCHAR(255) UNIQUE NOT NULL,
    password_hash   TEXT NOT NULL,
    display_name    VARCHAR(100),
    avatar_url      TEXT,
    role            VARCHAR(20) DEFAULT 'user'
                    CHECK (role IN ('user', 'admin', 'moderator')),
    is_active       BOOLEAN DEFAULT true,
    settings        JSONB DEFAULT '{}'::jsonb,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    last_login_at   TIMESTAMPTZ
);

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();


-- =========================================
-- 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. PROVIDER CONFIGS (replaces api_configs)
-- =========================================
-- Explicit scope enum replaces nullable column tri-state.
-- 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,
    api_key_enc     TEXT,
    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.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)';


-- =========================================
-- 5. MODEL CATALOG (replaces model_configs)
-- =========================================
-- Intrinsic capabilities for models the system knows about.
-- Hidden 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,
    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';

-- Fix CHECK constraint and default for existing databases (idempotent)
DO $$ BEGIN
    ALTER TABLE model_catalog DROP CONSTRAINT IF EXISTS model_catalog_visibility_check;
    -- Remap old values before adding new constraint
    UPDATE model_catalog SET visibility = 'enabled' WHERE visibility = 'visible';
    UPDATE model_catalog SET visibility = 'disabled' WHERE visibility = 'hidden';
    ALTER TABLE model_catalog ADD CONSTRAINT model_catalog_visibility_check
        CHECK (visibility IN ('enabled', 'disabled', 'team'));
    ALTER TABLE model_catalog ALTER COLUMN visibility SET DEFAULT 'disabled';
EXCEPTION WHEN OTHERS THEN NULL;
END $$;
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 'hidden by default — admin must enable for global models';
COMMENT ON COLUMN model_catalog.capabilities IS 'Intrinsic: {"streaming","tool_calling","vision","thinking","reasoning","code_optimized","web_search","max_context","max_output_tokens"}';


-- =========================================
-- 6. PERSONAS (replaces model_presets)
-- =========================================

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)';


-- =========================================
-- 7. PERSONA GRANTS (extensible resource binding)
-- =========================================

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 COLUMN persona_grants.grant_type IS 'Extensible: tool, knowledge_base (future), api_endpoint (future)';
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';


-- =========================================
-- 8. PLATFORM POLICIES (replaces scattered global_settings checks)
-- =========================================

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


-- =========================================
-- 9. GLOBAL SETTINGS (non-policy config)
-- =========================================
-- Retained for banner config, site branding, etc.
-- Policy-like keys move to platform_policies.

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

-- Seed defaults
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)
ON CONFLICT (key) DO NOTHING;


-- =========================================
-- 10. USER MODEL SETTINGS (replaces user_model_preferences)
-- =========================================

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();


-- =========================================
-- 11. CHANNELS (unified chats)
-- =========================================

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
    folder          TEXT,   -- backward compat: simple text folder name
    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';


-- =========================================
-- 12. 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';


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


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


-- =========================================
-- 15. FOLDERS & PROJECTS
-- =========================================

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();

-- Now add the FK from channels
DO $$ BEGIN
    ALTER TABLE channels ADD CONSTRAINT fk_channels_folder
        FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;

CREATE TABLE IF NOT EXISTS projects (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    name            VARCHAR(200) NOT NULL,
    description     TEXT,
    color           VARCHAR(7),
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_projects_user ON projects(user_id);
DROP TRIGGER IF EXISTS projects_updated_at ON projects;
CREATE TRIGGER projects_updated_at BEFORE UPDATE ON projects
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

CREATE TABLE IF NOT EXISTS project_channels (
    project_id  UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    channel_id  UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    added_at    TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (project_id, channel_id)
);

CREATE INDEX IF NOT EXISTS idx_project_channels_channel ON project_channels(channel_id);


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


-- =========================================
-- 17. 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';
