-- graph extension bootstrap SQL
-- Creates catalog tables for storing registered tables and edges.

DO $$
BEGIN
    CREATE TYPE graph.node_ref AS (
        node_table REGCLASS,
        node_id    TEXT
    );
EXCEPTION WHEN duplicate_object THEN
    NULL;
END
$$;

CREATE OR REPLACE FUNCTION graph.node_ref(node_table REGCLASS, node_id TEXT)
RETURNS graph.node_ref
LANGUAGE sql
IMMUTABLE
PARALLEL SAFE
AS $$
    SELECT (node_table, node_id)::graph.node_ref
$$;

CREATE OR REPLACE FUNCTION graph.traverse(
    starts        graph.node_ref[],
    max_depth     INTEGER DEFAULT (current_setting('graph.default_max_depth'))::INTEGER,
    edge_types    TEXT[] DEFAULT NULL,
    direction     TEXT DEFAULT 'any',
    node_tables   OID[] DEFAULT NULL,
    filter        JSONB DEFAULT NULL,
    tenant        TEXT DEFAULT NULL,
    strategy      TEXT DEFAULT 'bfs',
    uniqueness    TEXT DEFAULT 'node_global',
    include_start BOOLEAN DEFAULT true,
    hydrate       BOOLEAN DEFAULT true,
    max_rows      INTEGER DEFAULT 1000,
    row_offset    INTEGER DEFAULT 0,
    max_nodes     INTEGER DEFAULT (current_setting('graph.max_nodes'))::INTEGER,
    max_frontier  INTEGER DEFAULT (current_setting('graph.max_frontier'))::INTEGER
)
RETURNS TABLE (
    root_table OID,
    root_id    TEXT,
    node_table OID,
    node_id    TEXT,
    depth      INTEGER,
    path       JSONB,
    edge_path  JSONB,
    node       JSONB,
    root_table_name TEXT,
    node_table_name TEXT
)
LANGUAGE sql
STABLE
COST 1000
ROWS 1000
AS $$
    SELECT t.root_table,
           t.root_id,
           t.node_table,
           t.node_id,
           t.depth,
           t.path,
           t.edge_path,
           t.node,
           t.root_table_name,
           t.node_table_name
    FROM graph.traverse(
        ARRAY(SELECT start_ref.node_table::oid FROM unnest($1) AS start_ref),
        ARRAY(SELECT start_ref.node_id FROM unnest($1) AS start_ref),
        $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
    ) AS t;
$$;

CREATE TABLE IF NOT EXISTS graph._registered_tables (
    table_name TEXT PRIMARY KEY,
    id_column  TEXT NOT NULL,
    columns    TEXT DEFAULT '',
    tenant_column TEXT
);

CREATE TABLE IF NOT EXISTS graph._registered_edges (
    from_table    TEXT NOT NULL,
    from_column   TEXT NOT NULL,
    to_table      TEXT NOT NULL,
    to_column     TEXT NOT NULL,
    label         TEXT NOT NULL,
    bidirectional BOOLEAN DEFAULT true,
    weight_column TEXT,
    label_column  TEXT,
    UNIQUE (from_table, from_column, to_table, to_column, label)
);

ALTER TABLE graph._registered_edges
    ADD COLUMN IF NOT EXISTS label_column TEXT;

ALTER TABLE graph._registered_tables
    ADD COLUMN IF NOT EXISTS tenant_column TEXT;

CREATE TABLE IF NOT EXISTS graph._registered_filter_columns (
    table_name  TEXT NOT NULL,
    column_name TEXT NOT NULL,
    column_type TEXT NOT NULL DEFAULT 'numeric',
    UNIQUE (table_name, column_name)
);

ALTER TABLE graph._registered_filter_columns
    ADD COLUMN IF NOT EXISTS column_type TEXT NOT NULL DEFAULT 'numeric';

CREATE TABLE IF NOT EXISTS graph._build_jobs (
    build_id       TEXT PRIMARY KEY,
    status         TEXT NOT NULL CHECK (status IN ('queued', 'running', 'completed', 'failed')),
    nodes_loaded   BIGINT,
    edges_loaded   BIGINT,
    build_time_ms  DOUBLE PRECISION,
    memory_used_mb DOUBLE PRECISION,
    sync_mode      TEXT NOT NULL DEFAULT 'trigger',
    projection_mode TEXT NOT NULL DEFAULT 'csr_readonly'
        CHECK (projection_mode IN ('csr_readonly', 'mutable_overlay')),
    progress_phase TEXT NOT NULL DEFAULT 'queued',
    progress_message TEXT,
    started_at     TIMESTAMPTZ,
    finished_at    TIMESTAMPTZ,
    error          TEXT,
    worker_pid     INTEGER,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);

ALTER TABLE graph._build_jobs
    ADD COLUMN IF NOT EXISTS projection_mode TEXT,
    ADD COLUMN IF NOT EXISTS progress_phase TEXT,
    ADD COLUMN IF NOT EXISTS progress_message TEXT;

UPDATE graph._build_jobs
SET projection_mode = 'csr_readonly'
WHERE projection_mode IS NULL
   OR projection_mode NOT IN ('csr_readonly', 'mutable_overlay');

UPDATE graph._build_jobs
SET progress_phase = CASE status
    WHEN 'running' THEN 'building'
    ELSE status
END
WHERE progress_phase IS NULL;

UPDATE graph._build_jobs
SET progress_message = CASE status
    WHEN 'queued' THEN 'queued for background build'
    WHEN 'running' THEN 'building graph from registered source tables'
    WHEN 'completed' THEN 'build completed'
    WHEN 'failed' THEN COALESCE(error, 'build failed')
    ELSE progress_message
END
WHERE progress_message IS NULL;

ALTER TABLE graph._build_jobs
    ALTER COLUMN projection_mode SET DEFAULT 'csr_readonly',
    ALTER COLUMN projection_mode SET NOT NULL,
    ALTER COLUMN progress_phase SET DEFAULT 'queued',
    ALTER COLUMN progress_phase SET NOT NULL;

DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1
        FROM pg_catalog.pg_constraint
        WHERE conrelid = 'graph._build_jobs'::regclass
          AND conname = '_build_jobs_projection_mode_check'
    ) THEN
        ALTER TABLE graph._build_jobs
            ADD CONSTRAINT _build_jobs_projection_mode_check
            CHECK (projection_mode IN ('csr_readonly', 'mutable_overlay'));
    END IF;
END $$;

CREATE INDEX IF NOT EXISTS _build_jobs_status_idx
    ON graph._build_jobs (status, created_at);

CREATE TABLE IF NOT EXISTS graph._maintenance_jobs (
    job_id            TEXT PRIMARY KEY,
    status            TEXT NOT NULL CHECK (status IN ('queued', 'running', 'completed', 'failed')),
    sync_rows_applied BIGINT,
    nodes_after       BIGINT,
    edges_after       BIGINT,
    vacuum_time_ms    DOUBLE PRECISION,
    progress_phase    TEXT NOT NULL DEFAULT 'queued',
    progress_message  TEXT,
    started_at        TIMESTAMPTZ,
    finished_at       TIMESTAMPTZ,
    error             TEXT,
    worker_pid        INTEGER,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);

ALTER TABLE graph._maintenance_jobs
    ADD COLUMN IF NOT EXISTS progress_phase TEXT,
    ADD COLUMN IF NOT EXISTS progress_message TEXT;

UPDATE graph._maintenance_jobs
SET progress_phase = CASE status
    WHEN 'running' THEN 'rebuilding'
    ELSE status
END
WHERE progress_phase IS NULL;

UPDATE graph._maintenance_jobs
SET progress_message = CASE status
    WHEN 'queued' THEN 'queued for background maintenance'
    WHEN 'running' THEN 'rebuilding graph for maintenance'
    WHEN 'completed' THEN 'maintenance completed'
    WHEN 'failed' THEN COALESCE(error, 'maintenance failed')
    ELSE progress_message
END
WHERE progress_message IS NULL;

ALTER TABLE graph._maintenance_jobs
    ALTER COLUMN progress_phase SET DEFAULT 'queued',
    ALTER COLUMN progress_phase SET NOT NULL;

CREATE INDEX IF NOT EXISTS _maintenance_jobs_status_idx
    ON graph._maintenance_jobs (status, created_at);

CREATE TABLE IF NOT EXISTS graph._sync_log (
    id             BIGSERIAL PRIMARY KEY,
    op             CHAR(1) NOT NULL,
    table_oid      OID,
    table_name     TEXT NOT NULL,
    pk             TEXT,
    old_pk         TEXT,
    new_pk         TEXT,
    properties     JSONB,
    old_row        JSONB,
    new_row        JSONB,
    xid            BIGINT,
    needs_vacuum   BOOLEAN DEFAULT false,
    error_message  TEXT,
    created_at     TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_sync_log_id ON graph._sync_log (id);
CREATE INDEX IF NOT EXISTS idx_sync_log_created ON graph._sync_log (created_at);

CREATE TABLE IF NOT EXISTS graph._sync_buffer (
    id         BIGSERIAL PRIMARY KEY,
    op         CHAR(1) NOT NULL,
    table_name TEXT NOT NULL,
    pk         TEXT NOT NULL,
    old_pk     TEXT,
    new_pk     TEXT,
    properties JSONB,
    created_at TIMESTAMPTZ DEFAULT now()
);

ALTER TABLE graph._sync_buffer ADD COLUMN IF NOT EXISTS old_pk TEXT;
ALTER TABLE graph._sync_buffer ADD COLUMN IF NOT EXISTS new_pk TEXT;

CREATE INDEX IF NOT EXISTS idx_sync_buffer_created ON graph._sync_buffer (created_at);

-- Preserve extension-owned operational state across pg_dump/pg_restore.
-- Source tables remain authoritative for graph contents, but registered graph
-- catalogs, durable jobs, and unapplied sync rows are database state rather
-- than extension install metadata.
SELECT pg_catalog.pg_extension_config_dump('graph._registered_tables', '');
SELECT pg_catalog.pg_extension_config_dump('graph._registered_edges', '');
SELECT pg_catalog.pg_extension_config_dump('graph._registered_filter_columns', '');
SELECT pg_catalog.pg_extension_config_dump('graph._build_jobs', '');
SELECT pg_catalog.pg_extension_config_dump('graph._maintenance_jobs', '');
SELECT pg_catalog.pg_extension_config_dump('graph._sync_log', '');
SELECT pg_catalog.pg_extension_config_dump('graph._sync_log_id_seq', '');
SELECT pg_catalog.pg_extension_config_dump('graph._sync_buffer', '');
SELECT pg_catalog.pg_extension_config_dump('graph._sync_buffer_id_seq', '');

-- Do not run graph.auto_discover() during CREATE EXTENSION.
--
-- PostgreSQL records objects created while an extension script is running as
-- extension members. graph.auto_discover() calls graph.build(), and build uses
-- ON COMMIT DROP temp tables; if those temp tables are created inside the
-- extension transaction, PostgreSQL refuses to drop them because they are
-- marked as extension-owned. Users should run graph.auto_discover() after
-- CREATE EXTENSION completes.

-- ─── Privilege hardening ─────────────────────────────────────────────
-- Internal catalog tables should not be directly writable by non-admin
-- users. Access is mediated through the graph.* SQL API functions.
REVOKE ALL ON TABLE graph._registered_tables       FROM PUBLIC;
REVOKE ALL ON TABLE graph._registered_edges        FROM PUBLIC;
REVOKE ALL ON TABLE graph._registered_filter_columns FROM PUBLIC;
REVOKE ALL ON TABLE graph._build_jobs             FROM PUBLIC;
REVOKE ALL ON TABLE graph._maintenance_jobs       FROM PUBLIC;
REVOKE ALL ON TABLE graph._sync_log               FROM PUBLIC;
REVOKE ALL ON TABLE graph._sync_buffer            FROM PUBLIC;
GRANT SELECT ON TABLE graph._registered_tables       TO PUBLIC;
GRANT SELECT ON TABLE graph._registered_edges        TO PUBLIC;
GRANT SELECT ON TABLE graph._registered_filter_columns TO PUBLIC;
GRANT SELECT ON TABLE graph._build_jobs             TO PUBLIC;
GRANT SELECT ON TABLE graph._maintenance_jobs       TO PUBLIC;
GRANT SELECT ON TABLE graph._sync_log               TO PUBLIC;
GRANT SELECT ON TABLE graph._sync_buffer            TO PUBLIC;
GRANT SELECT ON SEQUENCE graph._sync_log_id_seq     TO PUBLIC;
GRANT SELECT ON SEQUENCE graph._sync_buffer_id_seq  TO PUBLIC;

-- Catalog mutation, build/vacuum, sync apply, reset, and global analytics are
-- protected in Rust by graph-admin checks. Production deployments should still
-- grant application roles only the reader functions they need.
