-- 0001_runs.sql — schema and tables for the run-level surface of Studio.
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6 (runs, run_steps).
-- ADR:  docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2/D7/D9.

CREATE SCHEMA IF NOT EXISTS stackbone_platform;

CREATE TABLE IF NOT EXISTS stackbone_platform.runs (
    id                  uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
    -- Nullable in local (single-agent emulator), required in cloud.
    agent_id            text,
    workspace_id        text,
    trigger             text        NOT NULL DEFAULT 'manual'
                                    CHECK (trigger IN ('manual', 'webhook', 'cron', 'event')),
    status              text        NOT NULL DEFAULT 'running'
                                    CHECK (status IN ('running', 'done', 'failed', 'interrupted')),
    trace_id            text,
    is_playground       boolean     NOT NULL DEFAULT false,
    input               jsonb,
    output              jsonb,
    error               jsonb,
    cost_estimated_usd  numeric,
    metadata            jsonb       NOT NULL DEFAULT '{}'::jsonb,
    created_at          timestamptz NOT NULL DEFAULT now(),
    started_at          timestamptz,
    finished_at         timestamptz,
    duration_ms         integer
);

CREATE INDEX IF NOT EXISTS runs_workspace_started_idx
    ON stackbone_platform.runs (workspace_id, started_at DESC NULLS LAST);

-- Drives the cursor pagination from §5 of the protocol spec: order by
-- created_at desc with id as the tie-break for runs created within the same
-- millisecond.
CREATE INDEX IF NOT EXISTS runs_created_at_id_idx
    ON stackbone_platform.runs (created_at DESC, id DESC);

CREATE TABLE IF NOT EXISTS stackbone_platform.run_steps (
    id              uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id          uuid        NOT NULL
                                REFERENCES stackbone_platform.runs(id) ON DELETE CASCADE,
    parent_step_id  uuid        REFERENCES stackbone_platform.run_steps(id) ON DELETE CASCADE,
    type            text        NOT NULL
                                CHECK (type IN (
                                    'agent', 'llm_call', 'db_query', 'http_fetch',
                                    'queue_publish', 'hitl_pause', 'tool_call',
                                    'rag_query', 'storage_op'
                                )),
    name            text,
    status          text        NOT NULL DEFAULT 'running'
                                CHECK (status IN ('ok', 'error', 'running')),
    payload         jsonb,
    error           jsonb,
    started_at      timestamptz NOT NULL DEFAULT now(),
    finished_at     timestamptz,
    duration_ms     integer
);

CREATE INDEX IF NOT EXISTS run_steps_run_started_idx
    ON stackbone_platform.run_steps (run_id, started_at);
CREATE INDEX IF NOT EXISTS run_steps_parent_idx
    ON stackbone_platform.run_steps (parent_step_id);

-- AFTER INSERT/UPDATE triggers fan-out into pg_notify('stackbone_platform_events',
-- ...). The `apps/api` SSE handler (D4) and the local emulator's bus (V2/V3)
-- LISTEN on this channel and forward each frame as the SSE payload defined in
-- the protocol spec §10.2.
CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_event() RETURNS trigger
LANGUAGE plpgsql AS $fn$
DECLARE
    payload jsonb;
BEGIN
    IF TG_OP = 'INSERT' THEN
        payload := jsonb_build_object(
            'type', 'run.created',
            'run_id', NEW.id,
            'trigger', NEW.trigger,
            'created_at', NEW.created_at
        );
    ELSIF TG_OP = 'UPDATE' THEN
        IF (OLD.started_at IS NULL) AND (NEW.started_at IS NOT NULL) THEN
            payload := jsonb_build_object(
                'type', 'run.started',
                'run_id', NEW.id,
                'started_at', NEW.started_at
            );
        ELSIF (OLD.finished_at IS NULL) AND (NEW.finished_at IS NOT NULL) THEN
            payload := jsonb_build_object(
                'type', 'run.finished',
                'run_id', NEW.id,
                'status', NEW.status,
                'finished_at', NEW.finished_at,
                'duration_ms', NEW.duration_ms
            );
        ELSE
            RETURN NULL;
        END IF;
    ELSE
        RETURN NULL;
    END IF;

    PERFORM pg_notify('stackbone_platform_events', payload::text);
    RETURN NULL;
END;
$fn$;

DROP TRIGGER IF EXISTS runs_notify_aiu ON stackbone_platform.runs;
CREATE TRIGGER runs_notify_aiu
AFTER INSERT OR UPDATE ON stackbone_platform.runs
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_event();

CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_step_event() RETURNS trigger
LANGUAGE plpgsql AS $fn$
DECLARE
    payload jsonb;
BEGIN
    IF TG_OP = 'INSERT' THEN
        payload := jsonb_build_object(
            'type', 'run.step.created',
            'run_id', NEW.run_id,
            'step_id', NEW.id,
            'parent_step_id', NEW.parent_step_id,
            'step_type', NEW.type,
            'started_at', NEW.started_at
        );
    ELSIF TG_OP = 'UPDATE' THEN
        IF (OLD.finished_at IS NULL) AND (NEW.finished_at IS NOT NULL) THEN
            payload := jsonb_build_object(
                'type', 'run.step.finished',
                'run_id', NEW.run_id,
                'step_id', NEW.id,
                'status', NEW.status,
                'duration_ms', NEW.duration_ms
            );
        ELSE
            RETURN NULL;
        END IF;
    ELSE
        RETURN NULL;
    END IF;

    PERFORM pg_notify('stackbone_platform_events', payload::text);
    RETURN NULL;
END;
$fn$;

DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
CREATE TRIGGER run_steps_notify_aiu
AFTER INSERT OR UPDATE ON stackbone_platform.run_steps
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_step_event();
