-- v1.1.9: durable per-app named queues (queue::*).
--
-- Producer: queue::enqueue(name, msg) — INSERTs into this table.
-- Consumer: a registered queue:receive trigger fires when a message is
-- available; the dispatcher claims with FOR UPDATE SKIP LOCKED + a
-- visibility-timeout window.
--
-- "The queue table IS the outbox" — there is no double-buffering. The
-- dispatcher's queue arm claims directly from queue_messages; the
-- visibility-timeout reclaim task resets stale claims so crashed
-- consumers don't lose work.
--
-- Identity tuple: (app_id, queue_name). Queue names are implicit — no
-- queue registry table; a queue exists once the first message is
-- enqueued under that name.

CREATE TABLE queue_messages (
    id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id                UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
    queue_name            TEXT NOT NULL,
    payload               JSONB NOT NULL,
    enqueued_at           TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    -- deliver_after: NULL = immediate; else dispatcher won't claim until NOW() >= deliver_after.
    deliver_after         TIMESTAMPTZ NULL,
    -- claim_token: NULL = unclaimed; UUID = currently leased by a dispatcher.
    claim_token           UUID NULL,
    claimed_at            TIMESTAMPTZ NULL,
    attempt               INT NOT NULL DEFAULT 0,
    max_attempts          INT NOT NULL DEFAULT 3,
    -- Forensic: who enqueued. NEVER used for authz — the trigger fires
    -- as the principal that REGISTERED the consumer (design notes §4).
    enqueued_by_principal UUID NULL
);

-- Dispatch hot path: scan for unclaimed messages in (app_id, queue_name)
-- order by enqueued_at. The (deliver_after IS NULL OR deliver_after <= NOW())
-- condition cannot live in the partial WHERE (NOW() is non-immutable);
-- Postgres applies it as a filter on the partial result set, which is
-- already small.
CREATE INDEX idx_queue_messages_dispatch
    ON queue_messages (app_id, queue_name, enqueued_at)
    WHERE claim_token IS NULL;

-- depth() / depth_pending() helpers and the dashboard's queue list view.
CREATE INDEX idx_queue_messages_app_queue
    ON queue_messages (app_id, queue_name);

-- Reclaim-task scan: find currently-leased messages whose claim is older
-- than the per-queue visibility_timeout_secs. Bounded by the number of
-- in-flight messages, which is small.
CREATE INDEX idx_queue_messages_claimed
    ON queue_messages (claimed_at)
    WHERE claim_token IS NOT NULL;
