-- Research Capacitor — Reservation runtime layer
-- Run this after `supabase/schema.sql` and `supabase/hardening.sql`.
--
-- Apply order:
--   1. supabase/schema.sql
--   2. supabase/hardening.sql
--   3. supabase/reservation_runtime.sql
--
-- Responsibility:
--   - canonical reservation-mode runtime RPCs
--   - reserve / transfer / release settlement
--   - lease recovery and runtime-mode reporting
--   - safe to rerun on upgraded projects

SET search_path TO public;

DO $$
BEGIN
    IF to_regclass('public.nodes') IS NULL
        OR to_regclass('public.credit_balances') IS NULL
        OR to_regclass('public.tasks') IS NULL
        OR to_regclass('public.task_results') IS NULL
        OR to_regclass('public.transactions') IS NULL THEN
        RAISE EXCEPTION 'Base schema missing in schema public. Run supabase/schema.sql first.';
    END IF;
END;
$$;

CREATE TABLE IF NOT EXISTS research_jobs (
    id                      uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    requester_id            uuid NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
    status                  text NOT NULL DEFAULT 'active',
    objective               text NOT NULL DEFAULT 'autonomous_research',
    domain                  text NOT NULL DEFAULT '',
    model                   text NOT NULL DEFAULT '',
    seed_conjecture         text NOT NULL DEFAULT '',
    total_budget            bigint NOT NULL DEFAULT 0 CHECK (total_budget >= 0),
    budget_per_attempt      bigint NOT NULL DEFAULT 0 CHECK (budget_per_attempt >= 0),
    max_concurrent_episodes integer NOT NULL DEFAULT 1 CHECK (max_concurrent_episodes > 0),
    max_attempts_per_episode integer NOT NULL DEFAULT 4 CHECK (max_attempts_per_episode > 0),
    credits_committed       bigint NOT NULL DEFAULT 0 CHECK (credits_committed >= 0),
    latest_error            text NOT NULL DEFAULT '',
    metadata                jsonb NOT NULL DEFAULT '{}'::jsonb,
    created_at              timestamptz NOT NULL DEFAULT now(),
    updated_at              timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS research_episodes (
    id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    job_id               uuid REFERENCES research_jobs(id) ON DELETE CASCADE,
    requester_id        uuid NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
    status              text NOT NULL DEFAULT 'active',
    domain              text NOT NULL DEFAULT '',
    conjecture_text     text NOT NULL,
    formal_statement    text NOT NULL DEFAULT '',
    strategy_tag        text NOT NULL DEFAULT '',
    interestingness_score double precision NOT NULL DEFAULT 0,
    novelty_score       double precision NOT NULL DEFAULT 0,
    max_attempts        integer NOT NULL DEFAULT 8 CHECK (max_attempts > 0),
    budget_per_attempt  bigint NOT NULL DEFAULT 0 CHECK (budget_per_attempt >= 0),
    attempt_count       integer NOT NULL DEFAULT 0,
    verified_attempt_id uuid,
    metadata            jsonb NOT NULL DEFAULT '{}'::jsonb,
    created_at          timestamptz NOT NULL DEFAULT now(),
    updated_at          timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS proof_attempts (
    id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    episode_id          uuid NOT NULL REFERENCES research_episodes(id) ON DELETE CASCADE,
    task_id             uuid UNIQUE REFERENCES tasks(id) ON DELETE SET NULL,
    task_result_id      uuid UNIQUE REFERENCES task_results(id) ON DELETE SET NULL,
    requester_id        uuid NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
    provider_id         uuid REFERENCES nodes(id) ON DELETE SET NULL,
    parent_attempt_id   uuid REFERENCES proof_attempts(id) ON DELETE SET NULL,
    attempt_index       integer NOT NULL DEFAULT 1,
    task_type           text NOT NULL DEFAULT 'proof_attempt',
    strategy_tag        text NOT NULL DEFAULT '',
    conjecture_text     text NOT NULL DEFAULT '',
    formal_statement    text NOT NULL DEFAULT '',
    prompt              text NOT NULL DEFAULT '',
    generated_lean_code text NOT NULL DEFAULT '',
    raw_output          text NOT NULL DEFAULT '',
    verification_status text NOT NULL DEFAULT 'pending',
    verification_summary text NOT NULL DEFAULT '',
    normalized_code     text NOT NULL DEFAULT '',
    lean_stdout         text NOT NULL DEFAULT '',
    lean_stderr         text NOT NULL DEFAULT '',
    verifier_metadata   jsonb NOT NULL DEFAULT '{}'::jsonb,
    novelty_score       double precision NOT NULL DEFAULT 0,
    interestingness_score double precision NOT NULL DEFAULT 0,
    created_at          timestamptz NOT NULL DEFAULT now(),
    updated_at          timestamptz NOT NULL DEFAULT now(),
    verified_at         timestamptz
);

CREATE TABLE IF NOT EXISTS verified_lemmas (
    id                   uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    source_task_id       text NOT NULL DEFAULT '',
    source_prompt        text NOT NULL DEFAULT '',
    source_job_id        text NOT NULL DEFAULT '',
    source_episode_id    text NOT NULL DEFAULT '',
    source_attempt_id    text NOT NULL DEFAULT '',
    metadata             jsonb NOT NULL DEFAULT '{}'::jsonb,
    conjecture           text NOT NULL DEFAULT '',
    normalized_code      text NOT NULL DEFAULT '',
    verification_status  text NOT NULL DEFAULT '',
    verification_summary text NOT NULL DEFAULT '',
    compiler             text NOT NULL DEFAULT '',
    stdout               text NOT NULL DEFAULT '',
    stderr               text NOT NULL DEFAULT '',
    is_true              boolean NOT NULL DEFAULT true,
    stored_at            timestamptz NOT NULL DEFAULT now()
);

ALTER TYPE txn_type ADD VALUE IF NOT EXISTS 'reserve';
ALTER TYPE txn_type ADD VALUE IF NOT EXISTS 'release';

ALTER TABLE credit_balances
    ADD COLUMN IF NOT EXISTS reserved_balance bigint NOT NULL DEFAULT 0;

ALTER TABLE nodes
    ADD COLUMN IF NOT EXISTS unlimited_budget boolean NOT NULL DEFAULT false;

ALTER TABLE task_results
    ADD COLUMN IF NOT EXISTS flops_used bigint NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS billed_mflops bigint NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS prompt_tokens integer NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS generated_tokens integer NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS model_params bigint NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS prefill_eval_ms double precision NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS decode_eval_ms double precision NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS total_eval_ms double precision NOT NULL DEFAULT 0;

ALTER TABLE tasks
    ADD COLUMN IF NOT EXISTS research_episode_id uuid,
    ADD COLUMN IF NOT EXISTS attempt_index integer NOT NULL DEFAULT 0,
    ADD COLUMN IF NOT EXISTS parent_attempt_id uuid,
    ADD COLUMN IF NOT EXISTS task_metadata jsonb NOT NULL DEFAULT '{}'::jsonb;

ALTER TABLE research_episodes
    ADD COLUMN IF NOT EXISTS job_id uuid REFERENCES research_jobs(id) ON DELETE CASCADE;

CREATE INDEX IF NOT EXISTS idx_tasks_episode ON tasks(research_episode_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status_created_at ON tasks(status, created_at);
CREATE INDEX IF NOT EXISTS idx_tasks_status_lease ON tasks(status, lease_expires_at);
CREATE INDEX IF NOT EXISTS idx_research_jobs_requester ON research_jobs(requester_id);
CREATE INDEX IF NOT EXISTS idx_research_jobs_status ON research_jobs(status);
CREATE INDEX IF NOT EXISTS idx_research_episodes_requester ON research_episodes(requester_id);
CREATE INDEX IF NOT EXISTS idx_research_episodes_status ON research_episodes(status);
CREATE INDEX IF NOT EXISTS idx_research_episodes_job ON research_episodes(job_id);
CREATE INDEX IF NOT EXISTS idx_proof_attempts_episode ON proof_attempts(episode_id);
CREATE INDEX IF NOT EXISTS idx_proof_attempts_episode_attempt ON proof_attempts(episode_id, attempt_index);
CREATE INDEX IF NOT EXISTS idx_proof_attempts_task ON proof_attempts(task_id);
CREATE INDEX IF NOT EXISTS idx_proof_attempts_status ON proof_attempts(verification_status);
CREATE INDEX IF NOT EXISTS idx_verified_lemmas_stored_at ON verified_lemmas(stored_at DESC);
CREATE INDEX IF NOT EXISTS idx_verified_lemmas_source_attempt ON verified_lemmas(source_attempt_id);
CREATE INDEX IF NOT EXISTS idx_transactions_created_at ON transactions(created_at DESC);

DROP TRIGGER IF EXISTS research_jobs_updated_at ON research_jobs;
CREATE TRIGGER research_jobs_updated_at
    BEFORE UPDATE ON research_jobs
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

DROP TRIGGER IF EXISTS research_episodes_updated_at ON research_episodes;
CREATE TRIGGER research_episodes_updated_at
    BEFORE UPDATE ON research_episodes
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

DROP TRIGGER IF EXISTS proof_attempts_updated_at ON proof_attempts;
CREATE TRIGGER proof_attempts_updated_at
    BEFORE UPDATE ON proof_attempts
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

CREATE OR REPLACE FUNCTION get_runtime_mode()
RETURNS text
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
    SELECT 'reservation-v2';
$$;

CREATE OR REPLACE FUNCTION get_known_model_param_count(
    p_model text
)
RETURNS bigint
LANGUAGE sql
IMMUTABLE
SET search_path = public
AS $$
    SELECT CASE COALESCE(p_model, '')
        WHEN 'mlx-community/DeepSeek-R1-Distill-Qwen-1.5B-4bit' THEN 1500000000::bigint
        WHEN 'bartowski/DeepSeek-R1-Distill-Qwen-1.5B-GGUF' THEN 1500000000::bigint
        WHEN 'mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit-mlx' THEN 7000000000::bigint
        WHEN 'bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF' THEN 7000000000::bigint
        WHEN 'mlx-community/DeepSeek-R1-Distill-Qwen-14B-4bit' THEN 14000000000::bigint
        WHEN 'bartowski/DeepSeek-R1-Distill-Qwen-14B-GGUF' THEN 14000000000::bigint
        WHEN 'mlx-community/DeepSeek-R1-Distill-Qwen-32B-4bit' THEN 32000000000::bigint
        WHEN 'bartowski/DeepSeek-R1-Distill-Qwen-32B-GGUF' THEN 32000000000::bigint
        ELSE NULL
    END;
$$;

CREATE OR REPLACE FUNCTION compute_runtime_mflops(
    p_model text,
    p_model_params bigint,
    p_prompt_tokens integer,
    p_generated_tokens integer
)
RETURNS bigint
LANGUAGE sql
IMMUTABLE
SET search_path = public
AS $$
    SELECT compute_runtime_flops(
        p_model,
        p_model_params,
        p_prompt_tokens,
        p_generated_tokens
    ) / 1000000;
$$;

CREATE OR REPLACE FUNCTION compute_runtime_flops(
    p_model text,
    p_model_params bigint,
    p_prompt_tokens integer,
    p_generated_tokens integer
)
RETURNS bigint
LANGUAGE sql
IMMUTABLE
SET search_path = public
AS $$
    SELECT
        2
        * GREATEST(
            COALESCE(NULLIF(GREATEST(COALESCE(p_model_params, 0), 0), 0), get_known_model_param_count(p_model), 0),
            0
        )
        * (
            GREATEST(COALESCE(p_prompt_tokens, 0), 0)
            + GREATEST(COALESCE(p_generated_tokens, 0), 0)
        );
$$;

DROP FUNCTION IF EXISTS compute_runtime_gflops(text, bigint, integer, integer);
DROP FUNCTION IF EXISTS compute_runtime_tflops(text, bigint, integer, integer);

CREATE OR REPLACE FUNCTION compute_runtime_tflops(
    p_model text,
    p_model_params bigint,
    p_prompt_tokens integer,
    p_generated_tokens integer
)
RETURNS numeric
LANGUAGE sql
IMMUTABLE
SET search_path = public
AS $$
    SELECT compute_runtime_flops(
            p_model,
            p_model_params,
            p_prompt_tokens,
            p_generated_tokens
        )::numeric / 1000000000000;
$$;

CREATE OR REPLACE FUNCTION compute_runtime_credit_subunits(
    p_model text,
    p_model_params bigint,
    p_prompt_tokens integer,
    p_generated_tokens integer
)
RETURNS bigint
LANGUAGE sql
IMMUTABLE
SET search_path = public
AS $$
    SELECT compute_runtime_flops(
        p_model,
        p_model_params,
        p_prompt_tokens,
        p_generated_tokens
    ) / 1000000;
$$;

DROP FUNCTION IF EXISTS get_balance(uuid, text);

CREATE OR REPLACE FUNCTION node_has_unlimited_budget(
    p_node_id uuid
)
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
    SELECT COALESCE((
        SELECT unlimited_budget
        FROM nodes
        WHERE id = p_node_id
    ), false);
$$;

CREATE OR REPLACE FUNCTION get_balance(
    p_node_id uuid,
    p_node_secret_hash text
)
RETURNS TABLE(balance bigint, reserved_balance bigint, total_balance bigint, updated_at timestamptz)
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
    SELECT cb.balance, cb.reserved_balance, cb.balance + cb.reserved_balance, cb.updated_at
    FROM credit_balances cb
    JOIN nodes n ON n.id = cb.node_id
    WHERE cb.node_id = p_node_id
      AND n.auth_token_hash = COALESCE(p_node_secret_hash, '');
$$;

CREATE OR REPLACE FUNCTION submit_task(
    p_node_id uuid,
    p_node_secret_hash text,
    p_prompt text,
    p_model text,
    p_budget bigint,
    p_task_type text DEFAULT 'lean_proof_gen',
    p_research_episode_id uuid DEFAULT NULL,
    p_attempt_index integer DEFAULT 0,
    p_parent_attempt_id uuid DEFAULT NULL,
    p_task_metadata jsonb DEFAULT '{}'::jsonb
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_balance bigint;
    v_task_id uuid;
    v_role node_role;
    v_unlimited boolean;
BEGIN
    IF p_budget <= 0 THEN
        RAISE EXCEPTION 'Budget must be positive';
    END IF;

    SELECT role INTO v_role
    FROM nodes
    WHERE id = p_node_id
      AND auth_token_hash = COALESCE(p_node_secret_hash, '');

    IF v_role IS NULL THEN
        RAISE EXCEPTION 'Requester authentication failed';
    END IF;

    IF v_role NOT IN ('requester', 'both') THEN
        RAISE EXCEPTION 'Node % is not allowed to submit tasks', p_node_id;
    END IF;

    v_unlimited := node_has_unlimited_budget(p_node_id);

    IF NOT v_unlimited THEN
        SELECT balance INTO v_balance
        FROM credit_balances
        WHERE node_id = p_node_id
        FOR UPDATE;

        IF v_balance IS NULL THEN
            RAISE EXCEPTION 'Node % not found in credit_balances', p_node_id;
        END IF;

        IF v_balance < p_budget THEN
            RAISE EXCEPTION 'Insufficient credits: have %, need %', v_balance, p_budget;
        END IF;

        UPDATE credit_balances
        SET balance = balance - p_budget,
            reserved_balance = reserved_balance + p_budget,
            updated_at = now()
        WHERE node_id = p_node_id;
    END IF;

    INSERT INTO tasks (
        requester_id,
        prompt,
        model,
        credit_budget,
        status,
        type,
        research_episode_id,
        attempt_index,
        parent_attempt_id,
        task_metadata
    )
    VALUES (
        p_node_id,
        p_prompt,
        p_model,
        p_budget,
        'pending',
        COALESCE(NULLIF(p_task_type, ''), 'lean_proof_gen'),
        p_research_episode_id,
        GREATEST(COALESCE(p_attempt_index, 0), 0),
        p_parent_attempt_id,
        COALESCE(p_task_metadata, '{}'::jsonb)
    )
    RETURNING id INTO v_task_id;

    IF NOT v_unlimited THEN
        INSERT INTO transactions (from_node_id, to_node_id, amount, type, task_id, memo)
        VALUES (p_node_id, NULL, p_budget, 'reserve', v_task_id, 'Task budget reserved');
    END IF;

    RETURN v_task_id;
END;
$$;

CREATE OR REPLACE FUNCTION backend_list_active_research_jobs()
RETURNS jsonb
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
    SELECT COALESCE(jsonb_agg(to_jsonb(j) ORDER BY j.updated_at ASC), '[]'::jsonb)
    FROM (
        SELECT *
        FROM research_jobs
        WHERE status IN ('active', 'pending')
        ORDER BY updated_at ASC
    ) j;
$$;

CREATE OR REPLACE FUNCTION backend_get_research_job_state(
    p_job_id uuid
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_job jsonb;
BEGIN
    SELECT jsonb_build_object(
        'id', j.id,
        'requester_id', j.requester_id,
        'status', j.status,
        'objective', j.objective,
        'domain', j.domain,
        'model', j.model,
        'seed_conjecture', j.seed_conjecture,
        'total_budget', j.total_budget,
        'budget_per_attempt', j.budget_per_attempt,
        'max_concurrent_episodes', j.max_concurrent_episodes,
        'max_attempts_per_episode', j.max_attempts_per_episode,
        'credits_committed', j.credits_committed,
        'latest_error', j.latest_error,
        'metadata', j.metadata,
        'created_at', j.created_at,
        'updated_at', j.updated_at,
        'episodes', COALESCE((
            SELECT jsonb_agg(
                jsonb_build_object(
                    'id', e.id,
                    'job_id', e.job_id,
                    'requester_id', e.requester_id,
                    'status', e.status,
                    'domain', e.domain,
                    'conjecture_text', e.conjecture_text,
                    'formal_statement', e.formal_statement,
                    'strategy_tag', e.strategy_tag,
                    'interestingness_score', e.interestingness_score,
                    'novelty_score', e.novelty_score,
                    'max_attempts', e.max_attempts,
                    'budget_per_attempt', e.budget_per_attempt,
                    'attempt_count', e.attempt_count,
                    'verified_attempt_id', e.verified_attempt_id,
                    'metadata', e.metadata,
                    'created_at', e.created_at,
                    'updated_at', e.updated_at,
                    'attempts', COALESCE((
                        SELECT jsonb_agg(to_jsonb(pa) ORDER BY pa.attempt_index ASC, pa.created_at ASC)
                        FROM proof_attempts pa
                        WHERE pa.episode_id = e.id
                    ), '[]'::jsonb)
                )
                ORDER BY e.updated_at DESC
            )
            FROM research_episodes e
            WHERE e.job_id = j.id
        ), '[]'::jsonb)
    )
    INTO v_job
    FROM research_jobs j
    WHERE j.id = p_job_id;

    RETURN v_job;
END;
$$;

CREATE OR REPLACE FUNCTION backend_create_research_episode(
    p_job_id uuid,
    p_conjecture_text text,
    p_formal_statement text DEFAULT '',
    p_domain text DEFAULT '',
    p_strategy_tag text DEFAULT '',
    p_metadata jsonb DEFAULT '{}'::jsonb,
    p_interestingness_score double precision DEFAULT 0,
    p_novelty_score double precision DEFAULT 0
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_job record;
    v_episode_id uuid;
BEGIN
    IF btrim(COALESCE(p_conjecture_text, '')) = '' THEN
        RAISE EXCEPTION 'Conjecture text is required';
    END IF;

    SELECT *
    INTO v_job
    FROM research_jobs
    WHERE id = p_job_id
    FOR UPDATE;

    IF v_job.id IS NULL THEN
        RAISE EXCEPTION 'Research job % not found', p_job_id;
    END IF;

    INSERT INTO research_episodes (
        job_id,
        requester_id,
        status,
        domain,
        conjecture_text,
        formal_statement,
        strategy_tag,
        interestingness_score,
        novelty_score,
        max_attempts,
        budget_per_attempt,
        metadata
    )
    VALUES (
        p_job_id,
        v_job.requester_id,
        'active',
        COALESCE(p_domain, v_job.domain, ''),
        p_conjecture_text,
        COALESCE(p_formal_statement, ''),
        COALESCE(p_strategy_tag, ''),
        COALESCE(p_interestingness_score, 0),
        COALESCE(p_novelty_score, 0),
        v_job.max_attempts_per_episode,
        v_job.budget_per_attempt,
        COALESCE(p_metadata, '{}'::jsonb)
    )
    RETURNING id INTO v_episode_id;

    RETURN v_episode_id;
END;
$$;

CREATE OR REPLACE FUNCTION backend_enqueue_research_attempt(
    p_job_id uuid,
    p_episode_id uuid,
    p_prompt text,
    p_model text DEFAULT '',
    p_task_type text DEFAULT 'proof_attempt',
    p_strategy_tag text DEFAULT '',
    p_parent_attempt_id uuid DEFAULT NULL,
    p_conjecture_text text DEFAULT NULL,
    p_formal_statement text DEFAULT NULL,
    p_metadata jsonb DEFAULT '{}'::jsonb
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_balance bigint;
    v_task_id uuid;
    v_attempt_id uuid;
    v_attempt_index integer;
    v_effective_budget bigint;
    v_job record;
    v_episode record;
    v_unlimited boolean;
BEGIN
    IF btrim(COALESCE(p_prompt, '')) = '' THEN
        RAISE EXCEPTION 'Attempt prompt is required';
    END IF;

    SELECT *
    INTO v_job
    FROM research_jobs
    WHERE id = p_job_id
    FOR UPDATE;

    IF v_job.id IS NULL THEN
        RAISE EXCEPTION 'Research job % not found', p_job_id;
    END IF;

    IF v_job.status NOT IN ('active', 'pending') THEN
        RAISE EXCEPTION 'Research job % is not active', p_job_id;
    END IF;

    SELECT *
    INTO v_episode
    FROM research_episodes
    WHERE id = p_episode_id
      AND job_id = p_job_id
    FOR UPDATE;

    IF v_episode.id IS NULL THEN
        RAISE EXCEPTION 'Research episode % not found for job %', p_episode_id, p_job_id;
    END IF;

    IF v_episode.status = 'verified' THEN
        RAISE EXCEPTION 'Research episode % is already verified', p_episode_id;
    END IF;

    IF v_episode.attempt_count >= v_episode.max_attempts THEN
        RAISE EXCEPTION 'Research episode % exhausted its attempt budget', p_episode_id;
    END IF;

    v_effective_budget := v_job.budget_per_attempt;
    IF COALESCE(v_effective_budget, 0) <= 0 THEN
        RAISE EXCEPTION 'Budget per attempt must be positive for job %', p_job_id;
    END IF;

    IF v_job.credits_committed + v_effective_budget > v_job.total_budget THEN
        RAISE EXCEPTION 'Research job % exhausted its total budget', p_job_id;
    END IF;

    v_unlimited := node_has_unlimited_budget(v_job.requester_id);

    IF NOT v_unlimited THEN
        SELECT balance INTO v_balance
        FROM credit_balances
        WHERE node_id = v_job.requester_id
        FOR UPDATE;

        IF v_balance IS NULL THEN
            RAISE EXCEPTION 'Requester % not found in credit_balances', v_job.requester_id;
        END IF;

        IF v_balance < v_effective_budget THEN
            RAISE EXCEPTION 'Insufficient credits: have %, need %', v_balance, v_effective_budget;
        END IF;
    END IF;

    v_attempt_index := v_episode.attempt_count + 1;

    IF NOT v_unlimited THEN
        UPDATE credit_balances
        SET balance = balance - v_effective_budget,
            reserved_balance = reserved_balance + v_effective_budget,
            updated_at = now()
        WHERE node_id = v_job.requester_id;
    END IF;

    INSERT INTO tasks (
        requester_id,
        provider_id,
        research_episode_id,
        type,
        prompt,
        model,
        status,
        credit_budget,
        attempt_index,
        parent_attempt_id,
        task_metadata
    )
    VALUES (
        v_job.requester_id,
        NULL,
        p_episode_id,
        COALESCE(NULLIF(p_task_type, ''), 'proof_attempt'),
        p_prompt,
        COALESCE(NULLIF(p_model, ''), v_job.model, ''),
        'pending',
        v_effective_budget,
        v_attempt_index,
        p_parent_attempt_id,
        COALESCE(p_metadata, '{}'::jsonb)
    )
    RETURNING id INTO v_task_id;

    INSERT INTO proof_attempts (
        episode_id,
        task_id,
        requester_id,
        parent_attempt_id,
        attempt_index,
        task_type,
        strategy_tag,
        conjecture_text,
        formal_statement,
        prompt,
        verification_status,
        novelty_score,
        interestingness_score
    )
    VALUES (
        p_episode_id,
        v_task_id,
        v_job.requester_id,
        p_parent_attempt_id,
        v_attempt_index,
        COALESCE(NULLIF(p_task_type, ''), 'proof_attempt'),
        COALESCE(p_strategy_tag, ''),
        COALESCE(p_conjecture_text, v_episode.conjecture_text),
        COALESCE(p_formal_statement, v_episode.formal_statement),
        p_prompt,
        'pending',
        COALESCE((p_metadata ->> 'novelty_score')::double precision, v_episode.novelty_score),
        COALESCE((p_metadata ->> 'interestingness_score')::double precision, v_episode.interestingness_score)
    )
    RETURNING id INTO v_attempt_id;

    UPDATE research_episodes
    SET status = 'active',
        attempt_count = v_attempt_index,
        updated_at = now()
    WHERE id = p_episode_id;

    UPDATE research_jobs
    SET status = 'active',
        credits_committed = credits_committed + v_effective_budget,
        updated_at = now()
    WHERE id = p_job_id;

    IF NOT v_unlimited THEN
        INSERT INTO transactions (from_node_id, to_node_id, amount, type, task_id, memo)
        VALUES (v_job.requester_id, NULL, v_effective_budget, 'reserve', v_task_id, 'Backend research attempt budget reserved');
    END IF;

    RETURN jsonb_build_object(
        'job_id', p_job_id,
        'episode_id', p_episode_id,
        'attempt_id', v_attempt_id,
        'task_id', v_task_id,
        'attempt_index', v_attempt_index,
        'budget', v_effective_budget
    );
END;
$$;

CREATE OR REPLACE FUNCTION backend_verify_research_attempt(
    p_attempt_id uuid,
    p_verification_status text,
    p_verification_summary text DEFAULT '',
    p_normalized_code text DEFAULT '',
    p_lean_stdout text DEFAULT '',
    p_lean_stderr text DEFAULT '',
    p_verifier_metadata jsonb DEFAULT '{}'::jsonb
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_attempt record;
BEGIN
    SELECT pa.*, re.id AS episode_ref, re.max_attempts, re.attempt_count, re.job_id
    INTO v_attempt
    FROM proof_attempts pa
    JOIN research_episodes re ON re.id = pa.episode_id
    WHERE pa.id = p_attempt_id
    FOR UPDATE OF pa, re;

    IF v_attempt.id IS NULL THEN
        RETURN false;
    END IF;

    UPDATE proof_attempts
    SET verification_status = COALESCE(NULLIF(p_verification_status, ''), 'failed'),
        verification_summary = COALESCE(p_verification_summary, ''),
        normalized_code = COALESCE(p_normalized_code, ''),
        lean_stdout = COALESCE(p_lean_stdout, ''),
        lean_stderr = COALESCE(p_lean_stderr, ''),
        verifier_metadata = COALESCE(p_verifier_metadata, '{}'::jsonb),
        verified_at = CASE
            WHEN COALESCE(NULLIF(p_verification_status, ''), 'failed') = 'verified' THEN now()
            ELSE NULL
        END,
        updated_at = now()
    WHERE id = p_attempt_id;

    IF COALESCE(NULLIF(p_verification_status, ''), 'failed') = 'verified' THEN
        UPDATE research_episodes
        SET status = 'verified',
            verified_attempt_id = p_attempt_id,
            updated_at = now()
        WHERE id = v_attempt.episode_id;
    ELSIF v_attempt.attempt_count >= v_attempt.max_attempts THEN
        UPDATE research_episodes
        SET status = 'exhausted',
            updated_at = now()
        WHERE id = v_attempt.episode_id
          AND status <> 'verified';
    END IF;

    RETURN true;
END;
$$;

CREATE OR REPLACE FUNCTION backend_update_research_job_status(
    p_job_id uuid,
    p_status text,
    p_latest_error text DEFAULT ''
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
    UPDATE research_jobs
    SET status = COALESCE(NULLIF(p_status, ''), status),
        latest_error = COALESCE(p_latest_error, latest_error),
        updated_at = now()
    WHERE id = p_job_id;

    RETURN FOUND;
END;
$$;

CREATE OR REPLACE FUNCTION store_verified_lemma(
    p_source_task_id text DEFAULT '',
    p_source_prompt text DEFAULT '',
    p_metadata jsonb DEFAULT '{}'::jsonb,
    p_conjecture text DEFAULT '',
    p_normalized_code text DEFAULT '',
    p_verification_status text DEFAULT '',
    p_verification_summary text DEFAULT '',
    p_compiler text DEFAULT '',
    p_stdout text DEFAULT '',
    p_stderr text DEFAULT '',
    p_is_true boolean DEFAULT true
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_id uuid;
BEGIN
    INSERT INTO verified_lemmas (
        source_task_id,
        source_prompt,
        source_job_id,
        source_episode_id,
        source_attempt_id,
        metadata,
        conjecture,
        normalized_code,
        verification_status,
        verification_summary,
        compiler,
        stdout,
        stderr,
        is_true
    )
    VALUES (
        COALESCE(p_source_task_id, ''),
        COALESCE(p_source_prompt, ''),
        COALESCE(p_metadata ->> 'job_id', ''),
        COALESCE(p_metadata ->> 'episode_id', ''),
        COALESCE(p_metadata ->> 'attempt_id', ''),
        COALESCE(p_metadata, '{}'::jsonb),
        COALESCE(p_conjecture, ''),
        COALESCE(p_normalized_code, ''),
        COALESCE(p_verification_status, ''),
        COALESCE(p_verification_summary, ''),
        COALESCE(p_compiler, ''),
        COALESCE(p_stdout, ''),
        COALESCE(p_stderr, ''),
        COALESCE(p_is_true, true)
    )
    RETURNING id INTO v_id;

    RETURN v_id;
END;
$$;

CREATE OR REPLACE FUNCTION requeue_expired_tasks()
RETURNS integer
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_task record;
    v_requeued integer := 0;
BEGIN
    FOR v_task IN
        SELECT id, requester_id, credit_budget, attempt_count, max_attempts
        FROM tasks
        WHERE status IN ('assigned', 'running')
          AND lease_expires_at IS NOT NULL
          AND lease_expires_at < now()
        FOR UPDATE SKIP LOCKED
    LOOP
        IF v_task.attempt_count >= v_task.max_attempts THEN
            UPDATE tasks
            SET status = 'failed',
                lease_expires_at = NULL,
                failed_at = now(),
                last_error = 'Task lease expired and retry budget was exhausted'
            WHERE id = v_task.id;

            UPDATE proof_attempts
            SET verification_status = 'expired',
                verification_summary = 'Task lease expired and retry budget was exhausted',
                lean_stderr = 'Task lease expired before verification completed.',
                updated_at = now()
            WHERE task_id = v_task.id;

            IF NOT node_has_unlimited_budget(v_task.requester_id) THEN
                UPDATE credit_balances
                SET balance = balance + v_task.credit_budget,
                    reserved_balance = GREATEST(reserved_balance - v_task.credit_budget, 0),
                    updated_at = now()
                WHERE node_id = v_task.requester_id;

                INSERT INTO transactions (from_node_id, to_node_id, amount, type, task_id, memo)
                VALUES (NULL, v_task.requester_id, v_task.credit_budget, 'release', v_task.id, 'Task reservation released after lease expiry');
            END IF;
        ELSE
            UPDATE tasks
            SET status = 'pending',
                provider_id = NULL,
                lease_expires_at = NULL,
                started_at = NULL,
                failed_at = now(),
                last_error = 'Task lease expired; requeued for another provider'
            WHERE id = v_task.id;

            UPDATE proof_attempts
            SET verification_status = 'requeued',
                verification_summary = 'Task lease expired and was requeued',
                lean_stderr = 'Task lease expired and was requeued for another provider.',
                provider_id = NULL,
                updated_at = now()
            WHERE task_id = v_task.id;
        END IF;

        v_requeued := v_requeued + 1;
    END LOOP;

    RETURN v_requeued;
END;
$$;

DROP FUNCTION IF EXISTS claim_task(uuid, text);

CREATE OR REPLACE FUNCTION claim_task(
    p_node_id uuid,
    p_node_secret_hash text,
    p_lease_seconds integer DEFAULT 1800
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_task_id uuid;
    v_role node_role;
    v_lease_seconds integer;
BEGIN
    PERFORM requeue_expired_tasks();

    v_lease_seconds := LEAST(GREATEST(COALESCE(p_lease_seconds, 1800), 5), 86400);

    SELECT role INTO v_role
    FROM nodes
    WHERE id = p_node_id
      AND auth_token_hash = COALESCE(p_node_secret_hash, '');

    IF v_role IS NULL THEN
        RAISE EXCEPTION 'Provider authentication failed';
    END IF;

    IF v_role NOT IN ('provider', 'both') THEN
        RAISE EXCEPTION 'Node % is not allowed to claim tasks', p_node_id;
    END IF;

    SELECT id INTO v_task_id
    FROM tasks
    WHERE status = 'pending'
    ORDER BY created_at ASC
    LIMIT 1
    FOR UPDATE SKIP LOCKED;

    IF v_task_id IS NULL THEN
        RETURN NULL;
    END IF;

    UPDATE tasks
    SET status = 'assigned',
        provider_id = p_node_id,
        attempt_count = attempt_count + 1,
        claimed_at = now(),
        started_at = NULL,
        completed_at = NULL,
        failed_at = NULL,
        lease_expires_at = now() + make_interval(secs => v_lease_seconds)
    WHERE id = v_task_id;

    RETURN v_task_id;
END;
$$;

DROP FUNCTION IF EXISTS mark_task_running(uuid, uuid, text);

CREATE OR REPLACE FUNCTION mark_task_running(
    p_task_id uuid,
    p_node_id uuid,
    p_node_secret_hash text,
    p_lease_seconds integer DEFAULT 1800
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_lease_seconds integer;
BEGIN
    v_lease_seconds := LEAST(GREATEST(COALESCE(p_lease_seconds, 1800), 5), 86400);

    UPDATE tasks
    SET status = 'running',
        started_at = now(),
        lease_expires_at = now() + make_interval(secs => v_lease_seconds)
    WHERE id = p_task_id
      AND provider_id = p_node_id
      AND EXISTS (
          SELECT 1
          FROM nodes
          WHERE id = p_node_id
            AND auth_token_hash = COALESCE(p_node_secret_hash, '')
      )
      AND status = 'assigned';

    RETURN FOUND;
END;
$$;

DROP FUNCTION IF EXISTS renew_task_lease(uuid, uuid, text);

CREATE OR REPLACE FUNCTION renew_task_lease(
    p_task_id uuid,
    p_node_id uuid,
    p_node_secret_hash text,
    p_lease_seconds integer DEFAULT 1800
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_lease_seconds integer;
BEGIN
    v_lease_seconds := LEAST(GREATEST(COALESCE(p_lease_seconds, 1800), 5), 86400);

    UPDATE tasks
    SET lease_expires_at = now() + make_interval(secs => v_lease_seconds)
    WHERE id = p_task_id
      AND provider_id = p_node_id
      AND EXISTS (
          SELECT 1
          FROM nodes
          WHERE id = p_node_id
            AND auth_token_hash = COALESCE(p_node_secret_hash, '')
      )
      AND status = 'running';

    RETURN FOUND;
END;
$$;

CREATE OR REPLACE FUNCTION fail_task(
    p_task_id uuid,
    p_node_id uuid,
    p_node_secret_hash text,
    p_error text DEFAULT ''
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_task record;
BEGIN
    SELECT id, requester_id, credit_budget, attempt_count, max_attempts, status, provider_id
    INTO v_task
    FROM tasks
    WHERE id = p_task_id
    FOR UPDATE;

    IF v_task.id IS NULL THEN
        RETURN false;
    END IF;

    IF v_task.provider_id IS DISTINCT FROM p_node_id THEN
        RETURN false;
    END IF;

    IF NOT EXISTS (
        SELECT 1
        FROM nodes
        WHERE id = p_node_id
          AND auth_token_hash = COALESCE(p_node_secret_hash, '')
    ) THEN
        RETURN false;
    END IF;

    IF v_task.status NOT IN ('assigned', 'running') THEN
        RETURN false;
    END IF;

    IF v_task.attempt_count >= v_task.max_attempts THEN
        UPDATE tasks
        SET status = 'failed',
            lease_expires_at = NULL,
            failed_at = now(),
            last_error = COALESCE(p_error, '')
        WHERE id = p_task_id;

        UPDATE proof_attempts
        SET verification_status = 'failed',
            verification_summary = COALESCE(p_error, ''),
            lean_stderr = COALESCE(p_error, ''),
            updated_at = now()
        WHERE task_id = p_task_id;

        IF NOT node_has_unlimited_budget(v_task.requester_id) THEN
            UPDATE credit_balances
            SET balance = balance + v_task.credit_budget,
                reserved_balance = GREATEST(reserved_balance - v_task.credit_budget, 0),
                updated_at = now()
            WHERE node_id = v_task.requester_id;

            INSERT INTO transactions (from_node_id, to_node_id, amount, type, task_id, memo)
            VALUES (NULL, v_task.requester_id, v_task.credit_budget, 'release', p_task_id, 'Task reservation released after final failure');
        END IF;
    ELSE
        UPDATE tasks
        SET status = 'pending',
            provider_id = NULL,
            lease_expires_at = NULL,
            started_at = NULL,
            failed_at = now(),
            last_error = COALESCE(p_error, '')
        WHERE id = p_task_id;

        UPDATE proof_attempts
        SET verification_status = 'requeued',
            verification_summary = COALESCE(p_error, ''),
            lean_stderr = COALESCE(p_error, ''),
            provider_id = NULL,
            updated_at = now()
        WHERE task_id = p_task_id;
    END IF;

    RETURN true;
END;
$$;

DROP FUNCTION IF EXISTS complete_task(uuid, uuid, text, text, bigint);
DROP FUNCTION IF EXISTS complete_task(uuid, uuid, text, text, bigint, integer, integer, bigint, double precision, double precision, double precision);

CREATE OR REPLACE FUNCTION complete_task(
    p_task_id uuid,
    p_node_id uuid,
    p_node_secret_hash text,
    p_lean_code text,
    p_raw_output text,
    p_mflops bigint,
    p_prompt_tokens integer DEFAULT 0,
    p_generated_tokens integer DEFAULT 0,
    p_model_params bigint DEFAULT 0,
    p_prefill_eval_ms double precision DEFAULT 0,
    p_decode_eval_ms double precision DEFAULT 0,
    p_total_eval_ms double precision DEFAULT 0
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_result_id uuid;
    v_task_status task_status;
    v_assigned_provider uuid;
    v_requester_id uuid;
    v_credit_budget bigint;
    v_task_model text;
    v_model_params bigint;
    v_prompt_tokens integer;
    v_generated_tokens integer;
    v_runtime_flops bigint;
    v_runtime_mflops bigint;
    v_runtime_credit_subunits bigint;
    v_billed_credits bigint;
    v_release_credits bigint;
BEGIN
    IF p_mflops < 0 THEN
        RAISE EXCEPTION 'MFLOPs must be non-negative';
    END IF;

    SELECT status, provider_id, requester_id, credit_budget, model
    INTO v_task_status, v_assigned_provider, v_requester_id, v_credit_budget, v_task_model
    FROM tasks
    WHERE id = p_task_id
    FOR UPDATE;

    IF v_task_status IS NULL THEN
        RAISE EXCEPTION 'Task % not found', p_task_id;
    END IF;

    IF v_task_status <> 'running' THEN
        RAISE EXCEPTION 'Task % must be running before completion, found %', p_task_id, v_task_status;
    END IF;

    IF v_assigned_provider IS DISTINCT FROM p_node_id THEN
        RAISE EXCEPTION 'Task % is not assigned to provider %', p_task_id, p_node_id;
    END IF;

    IF NOT EXISTS (
        SELECT 1
        FROM nodes
        WHERE id = p_node_id
          AND auth_token_hash = COALESCE(p_node_secret_hash, '')
    ) THEN
        RAISE EXCEPTION 'Provider authentication failed';
    END IF;

    IF btrim(COALESCE(p_lean_code, '')) = '' AND btrim(COALESCE(p_raw_output, '')) = '' THEN
        RAISE EXCEPTION 'Completion payload must include model output';
    END IF;

    v_prompt_tokens := GREATEST(COALESCE(p_prompt_tokens, 0), 0);
    v_generated_tokens := GREATEST(COALESCE(p_generated_tokens, 0), 0);
    IF v_prompt_tokens <= 0 THEN
        RAISE EXCEPTION 'Prompt token count must be positive';
    END IF;
    IF COALESCE(p_total_eval_ms, 0) <= 0 THEN
        RAISE EXCEPTION 'Total eval time must be positive';
    END IF;

    v_model_params := COALESCE(
        NULLIF(GREATEST(COALESCE(p_model_params, 0), 0), 0),
        get_known_model_param_count(v_task_model)
    );
    IF v_model_params IS NULL THEN
        RAISE EXCEPTION 'Model parameter count is required for unknown model %', COALESCE(v_task_model, '');
    END IF;

    v_runtime_flops := compute_runtime_flops(
        v_task_model,
        v_model_params,
        v_prompt_tokens,
        v_generated_tokens
    );
    v_runtime_mflops := compute_runtime_mflops(
        v_task_model,
        v_model_params,
        v_prompt_tokens,
        v_generated_tokens
    );
    v_runtime_credit_subunits := compute_runtime_credit_subunits(
        v_task_model,
        v_model_params,
        v_prompt_tokens,
        v_generated_tokens
    );
    v_billed_credits := LEAST(v_runtime_credit_subunits, v_credit_budget);
    v_release_credits := GREATEST(v_credit_budget - v_billed_credits, 0);

    UPDATE tasks
    SET status = 'completed',
        provider_id = p_node_id,
        completed_at = now(),
        lease_expires_at = NULL,
        last_error = ''
    WHERE id = p_task_id;

    INSERT INTO task_results (
        task_id,
        provider_id,
        lean_code,
        raw_output,
        flops_used,
        mflops_used,
        billed_mflops,
        prompt_tokens,
        generated_tokens,
        model_params,
        prefill_eval_ms,
        decode_eval_ms,
        total_eval_ms
    )
    VALUES (
        p_task_id,
        p_node_id,
        p_lean_code,
        p_raw_output,
        v_runtime_flops,
        v_runtime_mflops,
        v_billed_credits,
        v_prompt_tokens,
        v_generated_tokens,
        v_model_params,
        GREATEST(COALESCE(p_prefill_eval_ms, 0), 0),
        GREATEST(COALESCE(p_decode_eval_ms, 0), 0),
        GREATEST(COALESCE(p_total_eval_ms, 0), 0)
    )
    RETURNING id INTO v_result_id;

    UPDATE proof_attempts
    SET provider_id = p_node_id,
        task_result_id = v_result_id,
        generated_lean_code = COALESCE(p_lean_code, ''),
        raw_output = COALESCE(p_raw_output, ''),
        verification_status = CASE
            WHEN verification_status = 'verified' THEN verification_status
            ELSE 'generated'
        END,
        verification_summary = CASE
            WHEN verification_status = 'verified' THEN verification_summary
            ELSE 'Provider generated candidate proof; verification pending'
        END,
        updated_at = now()
    WHERE task_id = p_task_id;

    IF NOT node_has_unlimited_budget(v_requester_id) THEN
        UPDATE credit_balances
        SET balance = balance + v_release_credits,
            reserved_balance = GREATEST(reserved_balance - v_credit_budget, 0),
            updated_at = now()
        WHERE node_id = v_requester_id;
    END IF;

    IF v_billed_credits > 0 THEN
        INSERT INTO credit_balances (node_id, balance, reserved_balance, updated_at)
        VALUES (p_node_id, v_billed_credits, 0, now())
        ON CONFLICT (node_id)
        DO UPDATE SET balance = credit_balances.balance + v_billed_credits,
                      updated_at = now();

        INSERT INTO transactions (from_node_id, to_node_id, amount, type, task_id, memo)
        VALUES (v_requester_id, p_node_id, v_billed_credits, 'transfer', p_task_id, 'Task completion settlement');
    END IF;

    IF v_release_credits > 0 AND NOT node_has_unlimited_budget(v_requester_id) THEN
        INSERT INTO transactions (from_node_id, to_node_id, amount, type, task_id, memo)
        VALUES (NULL, v_requester_id, v_release_credits, 'release', p_task_id, 'Unused task reservation released');
    END IF;

    RETURN v_result_id;
END;
$$;

REVOKE ALL ON FUNCTION get_runtime_mode() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_runtime_mode() TO anon, authenticated;

REVOKE ALL ON FUNCTION get_known_model_param_count(text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_known_model_param_count(text) TO anon, authenticated;

REVOKE ALL ON FUNCTION compute_runtime_mflops(text, bigint, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION compute_runtime_mflops(text, bigint, integer, integer) TO anon, authenticated;

REVOKE ALL ON FUNCTION compute_runtime_flops(text, bigint, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION compute_runtime_flops(text, bigint, integer, integer) TO anon, authenticated;

REVOKE ALL ON FUNCTION compute_runtime_tflops(text, bigint, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION compute_runtime_tflops(text, bigint, integer, integer) TO anon, authenticated;

REVOKE ALL ON FUNCTION compute_runtime_credit_subunits(text, bigint, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION compute_runtime_credit_subunits(text, bigint, integer, integer) TO anon, authenticated;

REVOKE ALL ON FUNCTION get_balance(uuid, text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_balance(uuid, text) TO anon, authenticated;

REVOKE ALL ON FUNCTION submit_task(uuid, text, text, text, bigint, text, uuid, integer, uuid, jsonb) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION submit_task(uuid, text, text, text, bigint, text, uuid, integer, uuid, jsonb) TO anon, authenticated;

REVOKE ALL ON FUNCTION backend_list_active_research_jobs() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION backend_list_active_research_jobs() TO service_role;

REVOKE ALL ON FUNCTION backend_get_research_job_state(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION backend_get_research_job_state(uuid) TO service_role;

REVOKE ALL ON FUNCTION backend_create_research_episode(uuid, text, text, text, text, jsonb, double precision, double precision) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION backend_create_research_episode(uuid, text, text, text, text, jsonb, double precision, double precision) TO service_role;

REVOKE ALL ON FUNCTION backend_enqueue_research_attempt(uuid, uuid, text, text, text, text, uuid, text, text, jsonb) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION backend_enqueue_research_attempt(uuid, uuid, text, text, text, text, uuid, text, text, jsonb) TO service_role;

REVOKE ALL ON FUNCTION backend_verify_research_attempt(uuid, text, text, text, text, text, jsonb) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION backend_verify_research_attempt(uuid, text, text, text, text, text, jsonb) TO service_role;

REVOKE ALL ON FUNCTION backend_update_research_job_status(uuid, text, text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION backend_update_research_job_status(uuid, text, text) TO service_role;

REVOKE ALL ON FUNCTION store_verified_lemma(text, text, jsonb, text, text, text, text, text, text, text, boolean) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION store_verified_lemma(text, text, jsonb, text, text, text, text, text, text, text, boolean) TO service_role;

REVOKE ALL ON FUNCTION requeue_expired_tasks() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION requeue_expired_tasks() TO anon, authenticated;

REVOKE ALL ON FUNCTION claim_task(uuid, text, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION claim_task(uuid, text, integer) TO anon, authenticated;

REVOKE ALL ON FUNCTION mark_task_running(uuid, uuid, text, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION mark_task_running(uuid, uuid, text, integer) TO anon, authenticated;

REVOKE ALL ON FUNCTION renew_task_lease(uuid, uuid, text, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION renew_task_lease(uuid, uuid, text, integer) TO anon, authenticated;

REVOKE ALL ON FUNCTION fail_task(uuid, uuid, text, text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION fail_task(uuid, uuid, text, text) TO anon, authenticated;

REVOKE ALL ON FUNCTION complete_task(uuid, uuid, text, text, text, bigint, integer, integer, bigint, double precision, double precision, double precision) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION complete_task(uuid, uuid, text, text, text, bigint, integer, integer, bigint, double precision, double precision, double precision) TO anon, authenticated;
