-- v1.1.1: Universal trigger outbox — design notes §2.
--
-- One table for every async dispatch in the system. KV/cron/pubsub/
-- queue/email/dead-letter all write rows in this shape; the dispatcher
-- claims due rows with `FOR UPDATE SKIP LOCKED` and routes them to
-- the executor.
--
-- Sync HTTP also writes here (NATS-style inbox, design notes §3) —
-- `reply_to` carries an `inbox_id` that the orchestrator awaits on a
-- oneshot channel. `reply_to.is_some()` is the "don't retry" signal:
-- one attempt, surface the result via the inbox.
--
-- `trigger_id` is a polymorphic reference discriminated by
-- `source_kind`: for `source_kind='http'` it references `routes.id`;
-- otherwise it references `triggers.id`. Polymorphism handled in
-- Rust (the dispatcher); no DB-level FK because Postgres doesn't
-- support polymorphic FKs cleanly. NULL is allowed because direct
-- admin-replay paths may not have a triggering row at all.
--
-- `script_id` denormalized so the dispatcher resolves the target
-- script without an extra round-trip per row.

CREATE TABLE outbox (
    id                 UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id             UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
    source_kind        TEXT NOT NULL
        CHECK (source_kind IN ('http', 'kv', 'dead_letter')),
    -- Polymorphic — see comment above. No FK constraint.
    trigger_id         UUID,
    -- Pre-resolved at write time so the dispatcher doesn't re-look it up.
    script_id          UUID,
    -- NULL = async (retry per policy). Some(inbox_id) = sync HTTP
    -- (never retry; resolve the inbox with the result).
    reply_to           UUID,
    -- ServiceEvent + ExecRequest scaffold serialized as JSONB.
    payload            JSONB NOT NULL,
    -- Forensic field — the principal that triggered the originating
    -- event. NOT the execution principal for trigger fan-out (that
    -- comes from `triggers.registered_by_principal`).
    origin_principal   UUID,
    -- Trigger-depth as the dispatcher will hand it to the executor.
    -- Read out into ExecRequest.trigger_depth at dispatch time.
    trigger_depth      INT NOT NULL DEFAULT 0,
    -- Originating execution id (for audit log grouping). Equals the
    -- root for direct invocations; preserved across fan-out chains.
    root_execution_id  UUID,
    attempt_count      INT NOT NULL DEFAULT 0,
    next_attempt_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    -- Set inside the SELECT FOR UPDATE SKIP LOCKED transaction so
    -- the dispatcher can't double-pick a row across concurrent loop
    -- iterations.
    claimed_at         TIMESTAMPTZ,
    claimed_by         TEXT,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Hot index: the dispatcher's `WHERE next_attempt_at <= NOW() AND
-- claimed_at IS NULL` claim query. Partial index keeps the hot set
-- small even if the table grows large.
CREATE INDEX idx_outbox_due
    ON outbox (next_attempt_at)
    WHERE claimed_at IS NULL;

CREATE INDEX idx_outbox_app ON outbox (app_id);
