-- v1.2 Workflows (blueprint §9.1/§9.2): a durable DAG orchestration engine.
--
-- A `workflow` is a declarative graph of steps; each step invokes a function
-- (a script, by name, resolved in the app's scope) or a nested sub-workflow,
-- with `depends_on` edges, a per-step `when` condition, input mapping, and a
-- per-step retry + `on_error` policy. A `workflow::start(name, input)` (or the
-- admin run API) creates a RUN; a dedicated background orchestrator advances it
-- step-by-step, durably, surviving restarts.
--
-- Three tables:
--   * workflows          — the definition (owner-polymorphic like scripts/0050:
--                          app XOR group; M1 authors app-owned only, the group_id
--                          column ships unused so group-owned templates stay a
--                          door-open follow-up).
--   * workflow_runs      — one row per start(); the durable run record.
--   * workflow_run_steps — one row per step per run; carries the competing-
--                          consumer lease (claim_token/claimed_at) exactly like
--                          queue_messages (0034), so parallel steps complete
--                          lock-free and a crashed worker's step is reclaimable.

-- ---------------------------------------------------------------------------
-- workflows: the definition (owner-polymorphic, mirrors scripts/0050).
-- ---------------------------------------------------------------------------
CREATE TABLE workflows (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    -- exactly one of app_id / group_id (the exactly-one CHECK below). Code is
    -- not data, so RESTRICT (a group can't be deleted out from under a workflow
    -- it owns), mirroring scripts.
    app_id      UUID NULL REFERENCES apps(id)   ON DELETE CASCADE,
    group_id    UUID NULL REFERENCES groups(id) ON DELETE RESTRICT,
    name        TEXT NOT NULL,
    -- the parsed DAG: { "steps": [ { name, function|workflow, input, depends_on,
    -- when, retry, on_error } ] }. Validated server-side (acyclic, unique step
    -- names, deps exist) before it is ever written.
    definition  JSONB NOT NULL,
    enabled     BOOLEAN NOT NULL DEFAULT TRUE,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT workflows_owner_exactly_one CHECK ((group_id IS NULL) <> (app_id IS NULL))
);

-- Per-owner, case-insensitive name uniqueness (partial, one per owner column).
CREATE UNIQUE INDEX workflows_app_name_uidx
    ON workflows (app_id, LOWER(name)) WHERE app_id IS NOT NULL;
CREATE UNIQUE INDEX workflows_group_name_uidx
    ON workflows (group_id, LOWER(name)) WHERE group_id IS NOT NULL;
CREATE INDEX workflows_group_id_idx ON workflows (group_id) WHERE group_id IS NOT NULL;

-- ---------------------------------------------------------------------------
-- workflow_runs: one row per start(). Carries app_id NOT NULL (the data-plane
-- invariant — every run-listing query filters by app without a join) even
-- though it is derivable from the workflow.
-- ---------------------------------------------------------------------------
CREATE TABLE workflow_runs (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    workflow_id       UUID NOT NULL REFERENCES workflows(id) ON DELETE RESTRICT,
    app_id            UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
    status            TEXT NOT NULL DEFAULT 'pending'
                          CHECK (status IN ('pending', 'running', 'succeeded', 'failed', 'canceled')),
    input             JSONB NOT NULL DEFAULT '{}'::jsonb,
    output            JSONB NULL,
    error             TEXT NULL,
    -- correlates every step execution in execution_logs under one id.
    root_execution_id UUID NOT NULL,
    -- nesting: a sub-workflow run links back to the parent step that spawned it.
    workflow_depth    INT  NOT NULL DEFAULT 0,
    parent_run_id     UUID NULL REFERENCES workflow_runs(id) ON DELETE SET NULL,
    parent_step_id    UUID NULL,
    started_at        TIMESTAMPTZ NULL,
    finished_at       TIMESTAMPTZ NULL,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX workflow_runs_app_status_idx ON workflow_runs (app_id, status);
CREATE INDEX workflow_runs_workflow_idx   ON workflow_runs (workflow_id, created_at DESC);
CREATE INDEX workflow_runs_parent_idx     ON workflow_runs (parent_run_id) WHERE parent_run_id IS NOT NULL;

-- ---------------------------------------------------------------------------
-- workflow_run_steps: one row per step per run. The claim_token/claimed_at
-- lease is the queue_messages (0034) competing-consumer pattern — the
-- orchestrator claims a `ready` step FOR UPDATE SKIP LOCKED, and the reclaim
-- task frees a stale lease so a crashed worker's step is retried.
-- ---------------------------------------------------------------------------
CREATE TABLE workflow_run_steps (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id          UUID NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
    step_name       TEXT NOT NULL,
    status          TEXT NOT NULL DEFAULT 'pending'
                        CHECK (status IN ('pending', 'ready', 'running', 'succeeded', 'failed', 'skipped')),
    attempt         INT  NOT NULL DEFAULT 0,
    max_attempts    INT  NOT NULL DEFAULT 1,
    output          JSONB NULL,
    error           TEXT NULL,
    -- competing-consumer lease (mirrors queue_messages).
    claim_token     UUID NULL,
    claimed_at      TIMESTAMPTZ NULL,
    -- retry backoff / initial dispatch gate (NULL = due immediately once ready).
    next_attempt_at TIMESTAMPTZ NULL,
    -- set when this step is a nested sub-workflow: the child run it is waiting on.
    child_run_id    UUID NULL REFERENCES workflow_runs(id) ON DELETE SET NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (run_id, step_name)
);

-- The orchestrator's claim scan: ready steps whose backoff gate has elapsed.
-- (deliver-gate NOW() comparison is applied as a filter on the small partial
-- set, as in queue_messages.)
CREATE INDEX workflow_run_steps_claimable_idx
    ON workflow_run_steps (next_attempt_at)
    WHERE status = 'ready';

-- Advance re-eval + reclaim scans by run.
CREATE INDEX workflow_run_steps_run_idx ON workflow_run_steps (run_id);

-- Reclaim-task scan: leased steps whose claim is older than the visibility
-- timeout (bounded by in-flight step count).
CREATE INDEX workflow_run_steps_claimed_idx
    ON workflow_run_steps (claimed_at)
    WHERE claim_token IS NOT NULL;
