-- 0002_approvals.sql — HITL approvals queue and audit trail.
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.6/§6.7.
-- Component: docs/arquitectura/componentes/08-hitl-inbox.md.
-- ADR:  docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2.

CREATE TABLE IF NOT EXISTS stackbone_platform.approvals (
    id                      uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
    -- Nullable in local (single-agent emulator), required in cloud.
    agent_id                text,
    workspace_id            text,
    topic                   text        NOT NULL,
    status                  text        NOT NULL DEFAULT 'pending'
                                        CHECK (status IN (
                                            'pending', 'approved', 'rejected',
                                            'timed_out', 'cancelled'
                                        )),
    payload                 jsonb       NOT NULL DEFAULT '{}'::jsonb,
    schema                  jsonb,
    run_id                  uuid        REFERENCES stackbone_platform.runs(id) ON DELETE SET NULL,
    requested_by_step_id    uuid        REFERENCES stackbone_platform.run_steps(id) ON DELETE SET NULL,
    callback_url            text,
    idempotency_key         text,
    fallback                text        CHECK (fallback IS NULL OR fallback IN (
                                            'approve', 'reject', 'timeout'
                                        )),
    metadata                jsonb       NOT NULL DEFAULT '{}'::jsonb,
    created_at              timestamptz NOT NULL DEFAULT now(),
    timeout_at              timestamptz,
    decided_at              timestamptz
);

-- Idempotency: same (workspace, topic, idempotency_key) tuple cannot create
-- two approvals. Partial index keeps a NULL idempotency_key from blocking
-- everything; the 99% of HITL pauses that don't carry a key still work.
CREATE UNIQUE INDEX IF NOT EXISTS approvals_idempotency_uq
    ON stackbone_platform.approvals (workspace_id, topic, idempotency_key)
    WHERE idempotency_key IS NOT NULL;

CREATE INDEX IF NOT EXISTS approvals_workspace_status_idx
    ON stackbone_platform.approvals (workspace_id, status, created_at DESC);

CREATE INDEX IF NOT EXISTS approvals_run_idx
    ON stackbone_platform.approvals (run_id);

CREATE INDEX IF NOT EXISTS approvals_pending_timeout_idx
    ON stackbone_platform.approvals (timeout_at)
    WHERE status = 'pending' AND timeout_at IS NOT NULL;

CREATE TABLE IF NOT EXISTS stackbone_platform.approval_decisions (
    id              uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
    approval_id     uuid        NOT NULL
                                REFERENCES stackbone_platform.approvals(id) ON DELETE CASCADE,
    actor_user_id   text,
    actor_email     text,
    decision        text        NOT NULL CHECK (decision IN ('approve', 'reject')),
    edited_payload  jsonb,
    comment         text,
    decided_at      timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS approval_decisions_approval_idx
    ON stackbone_platform.approval_decisions (approval_id, decided_at);

-- Triggers fire on the same `stackbone_platform_events` channel as the runs
-- triggers, so the SSE handler in `apps/api` only LISTENs once.
CREATE OR REPLACE FUNCTION stackbone_platform.notify_approval_event() RETURNS trigger
LANGUAGE plpgsql AS $fn$
DECLARE
    payload jsonb;
BEGIN
    IF TG_OP = 'INSERT' THEN
        payload := jsonb_build_object(
            'type', 'approval.created',
            'approval_id', NEW.id,
            'run_id', NEW.run_id,
            'topic', NEW.topic,
            'created_at', NEW.created_at,
            'timeout_at', NEW.timeout_at
        );
    ELSIF TG_OP = 'UPDATE' THEN
        IF (OLD.decided_at IS NULL) AND (NEW.decided_at IS NOT NULL)
           AND NEW.status IN ('approved', 'rejected') THEN
            payload := jsonb_build_object(
                'type', 'approval.decided',
                'approval_id', NEW.id,
                'status', NEW.status,
                'decided_at', NEW.decided_at
            );
        ELSIF (OLD.status = 'pending') AND (NEW.status = 'timed_out') THEN
            payload := jsonb_build_object(
                'type', 'approval.timed_out',
                'approval_id', NEW.id,
                'fallback', NEW.fallback
            );
        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 approvals_notify_aiu ON stackbone_platform.approvals;
CREATE TRIGGER approvals_notify_aiu
AFTER INSERT OR UPDATE ON stackbone_platform.approvals
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_approval_event();
