


SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;


CREATE SCHEMA IF NOT EXISTS "public";


ALTER SCHEMA "public" OWNER TO "pg_database_owner";


COMMENT ON SCHEMA "public" IS 'standard public schema';



CREATE TYPE "public"."page_view_target_enum" AS ENUM (
    'thread',
    'thread_reply',
    'lens',
    'profile',
    'page',
    'battle'
);


ALTER TYPE "public"."page_view_target_enum" OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint DEFAULT 0, "p_output_tokens" bigint DEFAULT 0, "p_units" integer DEFAULT 1) RETURNS bigint
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  select "billing"."calculate_credit_cost"(p_model_id, p_input_tokens, p_output_tokens, p_units);
$$;


ALTER FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."custom_access_token_hook"("event" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
  v_claims    jsonb;
BEGIN
  v_claims := event -> 'claims';

  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = (event ->> 'user_id')::uuid
    AND type    = 'human'
  LIMIT 1;

  IF v_lenser_id IS NOT NULL THEN
    v_claims := jsonb_set(v_claims, '{lenser_id}', to_jsonb(v_lenser_id));
  END IF;

  RETURN jsonb_set(event, '{claims}', v_claims);
END;
$$;


ALTER FUNCTION "public"."custom_access_token_hook"("event" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") IS 'Supabase custom access token hook. Embeds lenser_id (human profile UUID) into JWT claims. Register in Dashboard → Auth → Hooks → Custom Access Token.';



CREATE OR REPLACE FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") RETURNS "text"
    LANGUAGE "sql" IMMUTABLE
    SET "search_path" TO 'public'
    AS $_$
  SELECT CASE p_table_name
    WHEN 'teams'                THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'evaluations'          THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'memory_profiles'      THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'model_profiles'       THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'personality_profiles' THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'scratchpad_runs'      THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'tool_assignments'     THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'tool_profiles'        THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'workflow_assignments' THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'workspace_settings'   THEN 'EXISTS (SELECT 1 FROM agents.ownerships o WHERE o.ai_lenser_id = tbl.ai_lenser_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'team_members'         THEN 'EXISTS (SELECT 1 FROM agents.teams t JOIN agents.ownerships o ON o.ai_lenser_id = t.ai_lenser_id WHERE t.id = tbl.team_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'team_edges'           THEN 'EXISTS (SELECT 1 FROM agents.teams t JOIN agents.ownerships o ON o.ai_lenser_id = t.ai_lenser_id WHERE t.id = tbl.team_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'agent_run_events'     THEN 'EXISTS (SELECT 1 FROM agents.team_runs tr JOIN agents.ownerships o ON o.ai_lenser_id = tr.ai_lenser_id WHERE tr.id = tbl.team_run_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'agent_run_steps'      THEN 'EXISTS (SELECT 1 FROM agents.team_runs tr JOIN agents.ownerships o ON o.ai_lenser_id = tr.ai_lenser_id WHERE tr.id = tbl.team_run_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'tools_registry'       THEN 'tbl.owner_lenser_id = $2'
    WHEN 'evaluation_cases'     THEN 'EXISTS (SELECT 1 FROM agents.evaluations e JOIN agents.ownerships o ON o.ai_lenser_id = e.ai_lenser_id WHERE e.id = tbl.evaluation_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'evaluation_rubrics'   THEN 'EXISTS (SELECT 1 FROM agents.evaluations e JOIN agents.ownerships o ON o.ai_lenser_id = e.ai_lenser_id WHERE e.id = tbl.evaluation_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
    WHEN 'evaluation_baselines' THEN 'EXISTS (SELECT 1 FROM agents.evaluations e JOIN agents.ownerships o ON o.ai_lenser_id = e.ai_lenser_id WHERE e.id = tbl.evaluation_id AND o.owner_lenser_id = $2 AND o.revoked_at IS NULL)'
  END
$_$;


ALTER FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") IS 'Internal helper: returns a parameterized SQL predicate expressing ownership of an agents.<table> row by lenser id $2. Used by fn_delete_workspace_item / fn_upsert_workspace_item to dispatch per-table ownership shapes safely.';



CREATE OR REPLACE FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_target_id uuid;
BEGIN
  SELECT id INTO v_target_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_target_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.relationships
  SET status = 'accepted',
      responded_at = now(),
      accepted_at  = now()
  WHERE source_profile_id = p_source_profile_id
    AND target_profile_id = v_target_id
    AND status = 'pending';

  IF NOT FOUND THEN
    RETURN jsonb_build_object('success', false, 'reason', 'no_pending_request');
  END IF;

  RETURN jsonb_build_object('success', true);
END;
$$;


ALTER FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean DEFAULT true) RETURNS TABLE("id" "uuid", "job_id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "error_code" "text", "error_message" "text", "attempt_count" integer, "payload" "jsonb", "resolved_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  RETURN QUERY
    SELECT
      d.id, d.job_id, d.battle_id, d.contender_id, d.slot,
      d.error_code, d.error_message, d.attempt_count, d.payload,
      d.resolved_at, d.created_at
    FROM battles.battle_execution_dead_letters d
    WHERE (NOT p_unresolved_only OR d.resolved_at IS NULL)
    ORDER BY d.created_at DESC
    LIMIT 200;
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer DEFAULT 30) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "updated_at" timestamp with time zone, "stale_seconds" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  RETURN QUERY
    SELECT
      b.id,
      b.slug,
      b.title,
      b.status::TEXT,
      b.updated_at,
      ROUND(EXTRACT(EPOCH FROM (now() - b.updated_at))::NUMERIC, 1) AS stale_seconds
    FROM battles.battles b
    WHERE b.status IN ('executing', 'scoring')
      AND b.updated_at < now() - (p_threshold_minutes || ' minutes')::INTERVAL
    ORDER BY b.updated_at ASC;
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text" DEFAULT 'pending'::"text", "p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 100) RETURNS SETOF "audit"."vote_anomalies"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'Forbidden: super_admin required' USING ERRCODE = 'P0003';
  END IF;

  RETURN QUERY
    SELECT *
      FROM audit.vote_anomalies
     WHERE (
       p_status = 'all'
       OR (p_status = 'pending'  AND resolved_at IS NULL)
       OR (p_status = 'resolved' AND resolved_at IS NOT NULL)
     )
       AND (p_battle_id IS NULL OR battle_id = p_battle_id)
     ORDER BY detected_at DESC
     LIMIT LEAST(p_limit, 500);
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) IS 'CA: Super-admin list helper for vote anomalies. Used by CLI and admin UI.';



CREATE OR REPLACE FUNCTION "public"."fn_admin_get_worker_health"() RETURNS TABLE("worker_id" "text", "worker_type" "text", "last_seen_at" timestamp with time zone, "is_healthy" boolean, "seconds_since" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  RETURN QUERY SELECT * FROM platform.fn_get_worker_health();
END;
$$;


ALTER FUNCTION "public"."fn_admin_get_worker_health"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_admin_health"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'cron', 'public'
    AS $$
DECLARE
  v_crons jsonb;
BEGIN
  SELECT jsonb_agg(jsonb_build_object(
    'name',      j.jobname,
    'schedule',  j.schedule,
    'active',    j.active,
    'last_run',  (
      SELECT start_time
      FROM   cron.job_run_details d
      WHERE  d.jobid = j.jobid
      ORDER  BY start_time DESC
      LIMIT  1
    )
  ))
  INTO v_crons
  FROM cron.job j;

  RETURN jsonb_build_object('crons', COALESCE(v_crons, '[]'::jsonb));
END;
$$;


ALTER FUNCTION "public"."fn_admin_health"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_admin_health"() IS 'Phase AI: returns pg_cron job list with last-run timestamps. Restricted to service_role. Used by health:cron gate script.';



CREATE OR REPLACE FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'INSUFFICIENT_PRIVILEGE';
  END IF;
  PERFORM battles.fn_retry_dead_letter_battle_job(p_dead_letter_id);
END;
$$;


ALTER FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") RETURNS "battles"."series"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid       UUID := auth.uid();
  v_series    battles.series%ROWTYPE;
  v_current   battles.series_rounds%ROWTYPE;
  v_winner    UUID;
  v_battle_id UUID;
  v_slug      TEXT;
  v_next      INT;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_series FROM battles.series WHERE id = p_series_id;
  IF v_series IS NULL THEN
    RAISE EXCEPTION 'series_not_found' USING ERRCODE = '42501';
  END IF;
  IF v_series.creator_lenser_id <> v_uid THEN
    RAISE EXCEPTION 'series_not_owned' USING ERRCODE = '42501';
  END IF;
  IF v_series.status = 'complete' THEN
    RETURN v_series;
  END IF;

  SELECT * INTO v_current
    FROM battles.series_rounds
   WHERE series_id    = p_series_id
     AND round_number = v_series.current_round;

  -- Resolve the winner from battles.battles.winner_contender_id; stamp it
  -- on the series_rounds row as the canonical record.
  SELECT b.winner_contender_id INTO v_winner
    FROM battles.battles b
   WHERE b.id = v_current.battle_id;
  IF v_winner IS NULL THEN
    RAISE EXCEPTION 'current_round_has_no_winner' USING ERRCODE = '22023';
  END IF;

  UPDATE battles.series_rounds
     SET winner_contender_id = v_winner
   WHERE id = v_current.id;

  v_next := v_series.current_round + 1;
  IF v_next > v_series.round_count THEN
    UPDATE battles.series
       SET status     = 'complete',
           updated_at = now()
     WHERE id = p_series_id
     RETURNING * INTO v_series;
    RETURN v_series;
  END IF;

  v_slug := lower(regexp_replace(v_series.title, '[^a-zA-Z0-9]+', '-', 'g'))
            || '-r' || v_next::text || '-' || substr(p_series_id::text, 1, 8);

  v_battle_id := public.fn_battles_create_from_template(
    v_series.template_id,
    v_series.title || ' — Round ' || v_next::text,
    v_slug
  );

  INSERT INTO battles.series_rounds (series_id, round_number, battle_id)
  VALUES (p_series_id, v_next, v_battle_id);

  UPDATE battles.series
     SET current_round = v_next,
         updated_at    = now()
   WHERE id = p_series_id
   RETURNING * INTO v_series;

  RETURN v_series;
END $$;


ALTER FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
BEGIN
  PERFORM battles.fn_advance_tournament(p_match_id);
END;
$$;


ALTER FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
    v_lenser_id  uuid;
    v_adapter_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'NOT_AUTHENTICATED';
    END IF;

    INSERT INTO agents.adapters (owner_lenser_id, name, adapter_type, config)
    VALUES (v_lenser_id, p_name, p_adapter_type, p_config)
    RETURNING id INTO v_adapter_id;

    RETURN v_adapter_id;
END;
$$;


ALTER FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'NOT_AUTHENTICATED';
    END IF;

    IF NOT EXISTS (
        SELECT 1 FROM agents.adapters
        WHERE id = p_adapter_id AND owner_lenser_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'NOT_FOUND_OR_FORBIDDEN';
    END IF;

    UPDATE agents.adapters
    SET is_active = false, updated_at = now()
    WHERE id = p_adapter_id AND owner_lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") RETURNS integer
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'audit', 'public'
    AS $$
  SELECT count(*)::INT
  FROM   audit.events
  WHERE  event_type = 'battle.joined'
  AND    payload->>'agent_id' = p_agent_id::TEXT
  AND    occurred_at >= date_trunc('day', now() AT TIME ZONE 'UTC');
$$;


ALTER FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") RETURNS TABLE("id" "uuid", "category" "text", "execution_mode" "text", "workflow_id" "uuid", "require_owner_approval" boolean, "max_joins_per_day" integer, "active" boolean, "daily_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
  SELECT
    bs.id,
    bs.category,
    bs.execution_mode,
    bs.workflow_id,
    bs.require_owner_approval,
    bs.max_joins_per_day,
    bs.active,
    public.fn_agent_daily_join_count(p_agent_id) AS daily_count,
    bs.created_at
  FROM  agents.battle_subscriptions bs
  JOIN  agents.ownerships o ON o.ai_lenser_id = bs.agent_id
  JOIN  lensers.profiles   p ON p.id           = o.owner_lenser_id
  WHERE bs.agent_id = p_agent_id
  AND   o.role IN ('owner', 'co_owner')
  AND   (o.revoked_at IS NULL OR o.revoked_at > now())
  AND   p.user_id = auth.uid()
  ORDER BY bs.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid" DEFAULT NULL::"uuid", "p_event_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "team_run_id" "uuid", "agent_run_step_id" "uuid", "event_type" "text", "payload" "jsonb", "occurred_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  -- Ownership guard: caller must be the owner or co-owner of this AI lenser.
  -- can_manage_ai_lenser resolves via lensers.get_auth_human_lenser_id(),
  -- which handles workspace-switching (agent_owner mode).
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT
    ev.id,
    ev.team_run_id,
    ev.agent_run_step_id,
    ev.event_type,
    ev.payload,
    ev.occurred_at
  FROM   agents.agent_run_events ev
  JOIN   agents.team_runs        tr ON tr.id = ev.team_run_id
  WHERE  tr.ai_lenser_id = p_ai_lenser_id
    AND  (p_run_id     IS NULL OR ev.team_run_id = p_run_id)
    AND  (p_event_type IS NULL OR ev.event_type  = p_event_type)
  ORDER BY ev.occurred_at DESC
  LIMIT  GREATEST(1, LEAST(COALESCE(p_limit, 100), 500));
END;
$$;


ALTER FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) IS 'Owner-only event stream for agents.agent_run_events. Filters by ai_lenser_id via team_runs join. Optional p_run_id and p_event_type narrowing. Authorization via agents.can_manage_ai_lenser (human owner profile). Max 500 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text" DEFAULT NULL::"text", "p_execution_mode" "text" DEFAULT 'cloud'::"text", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_require_approval" boolean DEFAULT false, "p_max_joins_per_day" integer DEFAULT 5) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_sub_id    UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships
    WHERE  ai_lenser_id    = p_agent_id
    AND    owner_lenser_id = v_lenser_id
    AND    role IN ('owner', 'co_owner')
  ) THEN
    RAISE EXCEPTION 'subscribe_forbidden: agent not owned by caller';
  END IF;

  INSERT INTO agents.battle_subscriptions (
    agent_id, category, execution_mode, workflow_id,
    require_owner_approval, max_joins_per_day
  )
  VALUES (
    p_agent_id, p_category, p_execution_mode, p_workflow_id,
    p_require_approval, p_max_joins_per_day
  )
  RETURNING id INTO v_sub_id;

  RETURN v_sub_id;
END;
$$;


ALTER FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  UPDATE agents.battle_subscriptions bs
  SET    active     = false,
         updated_at = now()
  FROM   agents.ai_lensers al
  JOIN   agents.ownerships  o ON o.ai_lenser_id    = al.id
                              AND o.owner_lenser_id = v_lenser_id
                              AND o.role IN ('owner', 'co_owner')
  WHERE  bs.id    = p_subscription_id
  AND    al.id    = bs.agent_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'unsubscribe_not_found_or_forbidden: %', p_subscription_id;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") RETURNS TABLE("id" "uuid", "provider_id" "uuid", "provider_key" "text", "provider_name" "text", "key" "text", "name" "text", "description" "text", "docs_url" "text", "support_level" "text", "status" "text", "capabilities" "text"[], "input_modalities" "text"[], "output_modalities" "text"[], "context_window_tokens" integer, "supports_tools" boolean, "supports_json_schema" boolean, "supports_vision" boolean, "supports_streaming" boolean, "use_cases" "text"[], "developer_summary" "text", "user_summary" "text", "metadata" "jsonb", "unit_type" "text", "cost_per_unit" numeric, "input_cost_per_1k_tokens" numeric, "output_cost_per_1k_tokens" numeric, "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT *
  FROM public.fn_ai_catalog_models(p_provider_key, NULL, NULL, NULL)
  WHERE key = p_model_key
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text" DEFAULT NULL::"text", "p_support_level" "text" DEFAULT NULL::"text", "p_capability" "text" DEFAULT NULL::"text", "p_modality" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "provider_id" "uuid", "provider_key" "text", "provider_name" "text", "key" "text", "name" "text", "description" "text", "docs_url" "text", "support_level" "text", "status" "text", "capabilities" "text"[], "input_modalities" "text"[], "output_modalities" "text"[], "context_window_tokens" integer, "supports_tools" boolean, "supports_json_schema" boolean, "supports_vision" boolean, "supports_streaming" boolean, "use_cases" "text"[], "developer_summary" "text", "user_summary" "text", "metadata" "jsonb", "unit_type" "text", "cost_per_unit" numeric, "input_cost_per_1k_tokens" numeric, "output_cost_per_1k_tokens" numeric, "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT
    m.id,
    m.provider_id,
    p.key AS provider_key,
    p.display_name AS provider_name,
    m.key,
    m.name,
    m.description,
    COALESCE(m.docs_url, m.provider_url),
    m.support_level,
    m.status,
    m.capabilities,
    m.input_modalities,
    m.output_modalities,
    m.context_window_tokens,
    m.supports_tools,
    m.supports_json_schema,
    m.supports_vision,
    m.supports_streaming,
    m.use_cases,
    m.developer_summary,
    m.user_summary,
    m.metadata,
    pricing.unit_type,
    pricing.cost_per_unit,
    pricing.input_cost_per_1k_tokens,
    pricing.output_cost_per_1k_tokens,
    m.is_active
  FROM ai.models m
  JOIN ai.providers p ON p.id = m.provider_id
  LEFT JOIN LATERAL (
    SELECT
      mp.unit_type::text AS unit_type,
      mp.cost_per_unit,
      mp.input_cost_per_1k_tokens,
      mp.output_cost_per_1k_tokens
    FROM ai.model_pricing mp
    WHERE mp.model_id = m.id
      AND mp.effective_from <= now()
      AND (mp.effective_to IS NULL OR mp.effective_to > now())
    ORDER BY mp.effective_from DESC
    LIMIT 1
  ) AS pricing ON TRUE
  WHERE (p_provider_key IS NULL OR p.key = p_provider_key)
    AND (p_support_level IS NULL OR m.support_level = p_support_level)
    AND (p_capability IS NULL OR p_capability = ANY (m.capabilities))
    AND (
      p_modality IS NULL OR
      p_modality = ANY (m.input_modalities) OR
      p_modality = ANY (m.output_modalities)
    )
  ORDER BY
    CASE m.support_level
      WHEN 'runnable' THEN 0
      WHEN 'byok_only' THEN 1
      WHEN 'catalog_only' THEN 2
      ELSE 3
    END,
    p.display_name,
    m.name;
$$;


ALTER FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_catalog_providers"() RETURNS TABLE("id" "uuid", "key" "text", "display_name" "text", "base_url" "text", "docs_url" "text", "support_level" "text", "logo_slug" "text", "metadata" "jsonb", "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT
    p.id,
    p.key,
    p.display_name,
    p.base_url,
    p.docs_url,
    p.support_level,
    p.logo_slug,
    p.metadata,
    p.is_active
  FROM ai.providers p
  ORDER BY
    CASE p.support_level
      WHEN 'runnable' THEN 0
      WHEN 'byok_only' THEN 1
      WHEN 'catalog_only' THEN 2
      ELSE 3
    END,
    p.display_name;
$$;


ALTER FUNCTION "public"."fn_ai_catalog_providers"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0, "p_media_kind" "text" DEFAULT NULL::"text", "p_ai_model_slug" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "ai_model_slug" "text", "lens_id" "uuid", "input_text" "text", "input_params" "jsonb", "output_type" "text", "visibility" "content"."visibility_enum", "created_at" timestamp with time zone, "original_chat_url" "text", "media" "jsonb")
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'ai', 'content', 'lensers', 'auth'
    AS $$
  select
    g.id,
    m.slug as ai_model_slug,
    g.lens_id,
    g.input_text,
    g.input_params,
    g.output_type,
    g.visibility,
    g.created_at,
    g.original_chat_url,
    jsonb_build_object(
      'id', ml.id,
      'url', ml.url,
      'storage_bucket', ml.storage_bucket,
      'file_name', ml.file_name,
      'display_name', ml.display_name,
      'mime_type', ml.mime_type,
      'extension', ml.extension,
      'size_bytes', ml.size_bytes,
      'width', ml.width,
      'height', ml.height,
      'duration_seconds', ml.duration_seconds,
      'media_kind', ml.media_kind,
      'source', ml.source,
      'meta', ml.meta,
      'created_at', ml.created_at
    ) as media
  from ai.generations g
  join ai.models m
    on m.id = g.ai_model_id
  join content.media_library ml
    on ml.id = g.media_id
  where
    g.lens_id = p_lens_id
    and g.lenser_id = p_lenser_id
    and (p_ai_model_slug is null or m.slug = p_ai_model_slug)
    and (p_media_kind is null or ml.media_kind = p_media_kind)
  order by g.created_at desc
  limit p_limit
  offset p_offset;
$$;


ALTER FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 5) RETURNS TABLE("product_tag" "text", "page" "text", "message" "text", "start_date" timestamp with time zone, "end_date" timestamp with time zone, "status" "text", "created_at" timestamp with time zone, "total_count" bigint)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'auth'
    AS $$
begin
  return query
  select
    f.product_tag::text,
    f.page,
    f.message,
    f.start_date,
    f.end_date,
    f.status::text,
    f.created_at,
    count(*) over() as total_count
  from analytics.product_feedback f
  where f.user_id = auth.uid()
  order by f.created_at desc
  offset p_offset
  limit p_limit;
end;
$$;


ALTER FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text" DEFAULT 'opened'::"text", "p_ip_hash" "text" DEFAULT NULL::"text", "p_user_agent" "text" DEFAULT NULL::"text", "p_referer" "text" DEFAULT NULL::"text", "p_country" "text" DEFAULT NULL::"text", "p_city" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'lensers', 'auth'
    AS $$
DECLARE
  v_user_id uuid;
  v_lenser_id uuid;
  v_link_id uuid;
BEGIN
  ------------------------------------------------------------
  -- Extract JWT user_id (NULL if anonymous)
  ------------------------------------------------------------
  BEGIN
    v_user_id := (auth.jwt() ->> 'sub')::uuid;
  EXCEPTION WHEN OTHERS THEN
    v_user_id := NULL;
  END;

  ------------------------------------------------------------
  -- Detect Lenser profile
  ------------------------------------------------------------
  IF v_user_id IS NOT NULL THEN
    SELECT id INTO v_lenser_id
    FROM lensers.profiles
    WHERE user_id = v_user_id;
  END IF;

  ------------------------------------------------------------
  -- Get link id from short_id
  ------------------------------------------------------------
  SELECT id INTO v_link_id
  FROM analytics.shared_links
  WHERE short_id = p_short_id;

  IF v_link_id IS NULL THEN
    RAISE EXCEPTION 'Shared link not found';
  END IF;

  ------------------------------------------------------------
  -- Insert event
  ------------------------------------------------------------
  INSERT INTO analytics.share_events (
    shared_link_id,
    event_type,
    viewer_lenser_id,
    ip_hash,
    user_agent,
    referer,
    country,
    city
  )
  VALUES (
    v_link_id,
    p_event_type,
    v_lenser_id,
    p_ip_hash,
    p_user_agent,
    p_referer,
    p_country,
    p_city
  );

END;
$$;


ALTER FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") RETURNS "analytics"."shared_links"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics'
    AS $$
DECLARE
    v_link analytics.shared_links;
BEGIN
    SELECT * INTO v_link
    FROM analytics.shared_links
    WHERE short_id = p_short_id;

    IF v_link.id IS NULL THEN
        RAISE EXCEPTION 'Shared link not found';
    END IF;

    IF NOT v_link.is_active THEN
        RAISE EXCEPTION 'Link is no longer active';
    END IF;

    IF v_link.expires_at IS NOT NULL AND now() > v_link.expires_at THEN
        UPDATE analytics.shared_links SET is_active = false WHERE id = v_link.id;
        RAISE EXCEPTION 'Link has expired';
    END IF;

    IF v_link.max_uses IS NOT NULL AND v_link.use_count >= v_link.max_uses THEN
        UPDATE analytics.shared_links SET is_active = false WHERE id = v_link.id;
        RAISE EXCEPTION 'Link usage limit reached';
    END IF;

    -- Increment use count
    UPDATE analytics.shared_links
    SET use_count = use_count + 1,
        is_active = CASE
            WHEN max_uses IS NOT NULL AND use_count + 1 >= max_uses THEN false
            ELSE is_active
        END
    WHERE id = v_link.id
    RETURNING * INTO v_link;

    RETURN v_link;
END;
$$;


ALTER FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text" DEFAULT NULL::"text", "p_slug" "text" DEFAULT NULL::"text", "p_channel" "text" DEFAULT 'in_app'::"text", "p_meta" "jsonb" DEFAULT '{}'::"jsonb", "p_display_name" "text" DEFAULT NULL::"text", "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_max_uses" integer DEFAULT NULL::integer) RETURNS "analytics"."shared_links"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'lensers', 'auth'
    AS $$
DECLARE
    v_user_id       uuid;
    v_lenser_id     uuid;
    v_resource_uuid uuid;
    v_short_id      text;
    v_new           analytics.shared_links;
    v_handle        text;
    v_recent_count  integer;
BEGIN
    -- 1. Identify the creator from JWT
    BEGIN
        v_user_id := (auth.jwt() ->> 'sub')::uuid;
    EXCEPTION WHEN OTHERS THEN
        v_user_id := NULL;
    END;

    IF v_user_id IS NOT NULL THEN
        SELECT id, handle INTO v_lenser_id, v_handle
        FROM lensers.profiles
        WHERE user_id = v_user_id;
    END IF;

    -- 2. Anti-abuse rate limiting
    SELECT COUNT(*) INTO v_recent_count
    FROM analytics.shared_links
    WHERE creator_lenser_id = v_lenser_id
      AND created_at > now() - interval '1 hour';

    IF v_recent_count >= 100 THEN
        RAISE EXCEPTION 'Rate limit exceeded: too many shared links created recently';
    END IF;

    -- 3. Validate resource_type
    IF p_resource_type NOT IN (
        'thread', 'lens', 'battle', 'profile', 'tag',
        'group_invite', 'org_member_invite', 'battle_invite',
        'workflow', 'page'
    ) THEN
        RAISE EXCEPTION 'Invalid resource_type: %', p_resource_type;
    END IF;

    -- 4. Parse and validate resource_id
    IF p_resource_id IS NOT NULL THEN
        BEGIN
            v_resource_uuid := p_resource_id::uuid;
        EXCEPTION WHEN OTHERS THEN
            RAISE EXCEPTION 'Invalid resource_id format: must be a valid UUID';
        END;

        -- Validate resource exists for invitation types
        IF p_resource_type = 'group_invite' THEN
            IF NOT EXISTS (SELECT 1 FROM lensers.groups WHERE id = v_resource_uuid) THEN
                RAISE EXCEPTION 'Group not found: %', v_resource_uuid;
            END IF;
        ELSIF p_resource_type = 'org_member_invite' THEN
            IF NOT EXISTS (SELECT 1 FROM organizations.organizations WHERE id = v_resource_uuid) THEN
                RAISE EXCEPTION 'Organization not found: %', v_resource_uuid;
            END IF;
        ELSIF p_resource_type = 'battle_invite' THEN
            IF NOT EXISTS (SELECT 1 FROM battles.battles WHERE id = v_resource_uuid) THEN
                RAISE EXCEPTION 'Battle not found: %', v_resource_uuid;
            END IF;
        END IF;
    END IF;

    -- 5. Generate short_id (using gen_random_uuid to avoid pgcrypto dependency)
    v_short_id := replace(gen_random_uuid()::text, '-', '');
    v_short_id := substr(v_short_id, 1, 8);

    -- 6. Insert shared link
    INSERT INTO analytics.shared_links (
        short_id, resource_type, resource_id, slug,
        creator_lenser_id, channel, meta, display_name,
        expires_at, max_uses
    )
    VALUES (
        v_short_id, p_resource_type, v_resource_uuid, p_slug,
        v_lenser_id, p_channel, p_meta,
        COALESCE(p_display_name, v_handle),
        p_expires_at, p_max_uses
    )
    RETURNING * INTO v_new;

    RETURN v_new;
END;
$$;


ALTER FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) IS 'Creates a shared or invitation link. Supports group_invite, org_member_invite, and battle_invite resource types. Added p_expires_at and p_max_uses in migration 000030. 6-param overload removed in migration 000054 to resolve PostgREST 42725 ambiguity. SECURITY DEFINER — rate-limited to 100 links/hour per lenser.';



CREATE OR REPLACE FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") RETURNS "analytics"."shared_links"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics'
    AS $$
DECLARE
  v_link analytics.shared_links;
BEGIN
  SELECT *
  INTO v_link
  FROM analytics.shared_links
  WHERE short_id = p_short_id;

  IF v_link.id IS NULL THEN
    RAISE EXCEPTION 'Shared link not found';
  END IF;

  RETURN v_link;
END;
$$;


ALTER FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text" DEFAULT 'general'::"text", "p_page" "text" DEFAULT NULL::"text", "p_message" "text" DEFAULT NULL::"text", "p_start_date" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_end_date" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'auth'
    AS $$
DECLARE
  v_user_id uuid;
BEGIN
  v_user_id := auth.uid();

  -- Must be authenticated
  IF v_user_id IS NULL THEN
    RAISE EXCEPTION 'authentication required' USING ERRCODE = '42501';
  END IF;

  -- Message length guard (matches service-layer validation)
  IF p_message IS NOT NULL AND (length(trim(p_message)) < 10 OR length(trim(p_message)) > 2000) THEN
    RAISE EXCEPTION 'feedback message must be between 10 and 2000 characters' USING ERRCODE = '22023';
  END IF;

  -- Validate product_tag before cast to surface a clear error to the client
  IF p_product_tag NOT IN ('bug', 'feature', 'ui_ux', 'general', 'other') THEN
    RAISE EXCEPTION 'invalid product_tag: %. Must be one of: bug, feature, ui_ux, general, other', p_product_tag
      USING ERRCODE = '22023';
  END IF;

  INSERT INTO analytics.product_feedback (
    user_id,
    product_tag,
    page,
    message,
    start_date,
    end_date
  ) VALUES (
    v_user_id,
    p_product_tag::analytics.product_tag_enum,  -- FIX: explicit cast (42804)
    p_page,
    trim(p_message),
    p_start_date,
    p_end_date
  );
END;
$$;


ALTER FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) IS 'Inserts a product feedback row for the calling user. Validates message length and product_tag enum value server-side. Fixed in migration 000055: added explicit ::analytics.product_tag_enum cast and enum value guard to resolve 42804 type mismatch.';



CREATE OR REPLACE FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text" DEFAULT 'general'::"text", "p_page" "text" DEFAULT NULL::"text", "p_message" "text" DEFAULT NULL::"text", "p_start_date" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_end_date" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'auth'
    AS $$
DECLARE
  v_user_id uuid;
  v_recent_count int;
BEGIN
  -- Capture caller identity if authenticated (NULL is fine for anon)
  v_user_id := auth.uid();

  -- Message validation
  IF p_message IS NOT NULL AND (
    length(trim(p_message)) < 10 OR
    length(trim(p_message)) > 2000
  ) THEN
    RAISE EXCEPTION 'feedback message must be between 10 and 2000 characters'
      USING ERRCODE = '22023';
  END IF;

  -- Product tag validation
  IF p_product_tag NOT IN ('bug', 'feature', 'ui_ux', 'general', 'other') THEN
    RAISE EXCEPTION 'invalid product_tag: %. Must be one of: bug, feature, ui_ux, general, other',
      p_product_tag USING ERRCODE = '22023';
  END IF;

  -- Rate-limit authenticated users: max 5 per hour
  IF v_user_id IS NOT NULL THEN
    SELECT COUNT(*)
      INTO v_recent_count
      FROM analytics.product_feedback
     WHERE user_id = v_user_id
       AND created_at > now() - interval '1 hour';

    IF v_recent_count >= 5 THEN
      RAISE EXCEPTION 'rate limit exceeded: max 5 feedback submissions per hour'
        USING ERRCODE = '429';
    END IF;
  END IF;

  INSERT INTO analytics.product_feedback (
    user_id,
    product_tag,
    page,
    message,
    start_date,
    end_date
  ) VALUES (
    v_user_id,
    p_product_tag::analytics.product_tag_enum,
    p_page,
    trim(p_message),
    p_start_date,
    p_end_date
  );
END;
$$;


ALTER FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) IS 'Public (anon-accessible) feedback submission. Captures auth.uid() when authenticated. Rate-limits authenticated callers to 5/hour. Anon rate-limiting is handled client-side.';



CREATE OR REPLACE FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  INSERT INTO agents.agent_run_events (team_run_id, event_type, payload, occurred_at)
  VALUES (p_team_run_id, p_event_type, p_payload, now());
$$;


ALTER FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("event_id" bigint, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_caller_id uuid := lensers.get_auth_lenser_id();
  v_next_id bigint;
  v_created_at timestamptz;
BEGIN
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'insufficient_privilege';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    WHERE r.id = p_run_id
      AND r.triggered_by = v_caller_id
  ) THEN
    RAISE EXCEPTION 'workflow_run % not found or not owned by caller', p_run_id
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  -- Serialize per-run event id allocation.
  PERFORM pg_advisory_xact_lock(hashtext(p_run_id::text));

  SELECT COALESCE(MAX(e.event_id), 0) + 1
    INTO v_next_id
  FROM lenses.workflow_run_events e
  WHERE e.run_id = p_run_id;

  INSERT INTO lenses.workflow_run_events (run_id, event_id, type, payload)
  VALUES (p_run_id, v_next_id, p_type, COALESCE(p_payload, '{}'::jsonb))
  RETURNING workflow_run_events.created_at INTO v_created_at;

  RETURN QUERY SELECT v_next_id, v_created_at;
END;
$$;


ALTER FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") IS 'Appends a workflow run event with monotonic per-run event_id. Owner-gated.';



CREATE OR REPLACE FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'media', 'public'
    AS $$
BEGIN
  UPDATE lenses.workflow_runs
  SET media_manifest = media_manifest || jsonb_build_object(
    'object_id',  p_object_id,
    'media_type', p_media_type,
    'mime_type',  p_mime_type,
    'node_id',    p_node_id,
    'added_at',   now()
  )
  WHERE id = p_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") IS 'AP: Appends a media object entry to lenses.workflow_runs.media_manifest. Called by the WorkflowExecutionService after fn_complete_async_run succeeds. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_inv agents.tool_invocations;
BEGIN
  SELECT * INTO v_inv FROM agents.tool_invocations WHERE id = p_invocation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'invocation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_inv.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF v_inv.approval_status <> 'pending' THEN
    RAISE EXCEPTION 'invocation is not pending approval' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.tool_invocations
     SET approval_status     = 'approved',
         approval_decided_by = auth.uid(),
         status              = 'running',
         started_at          = now()
   WHERE id = p_invocation_id;
END;
$$;


ALTER FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_archive('agent', p_ai_lenser_id); $$;


ALTER FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_archive('lens', p_lens_id); $$;


ALTER FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_archive('workflow', p_workflow_id); $$;


ALTER FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type  TEXT := lower(trim(p_artifact_type));
  v_actor UUID := lensers.get_auth_lenser_id();
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  IF v_type = 'lens' THEN
    UPDATE lenses.lenses
       SET archived_at = COALESCE(archived_at, now()),
           status = 'archived'::content.content_status,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    UPDATE lenses.workflows
       SET archived_at = COALESCE(archived_at, now()),
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    UPDATE battles.battles
       SET archived_at = COALESCE(archived_at, now()),
           status = 'archived'::battles.battle_status_enum,
           updated_at = now()
     WHERE id = p_artifact_id;
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (p_artifact_id, 'archived', v_actor, jsonb_build_object('action', 'archived'));
  ELSIF v_type = 'agent' THEN
    UPDATE agents.ai_lensers
       SET archived_at = COALESCE(archived_at, now()),
           is_active = false,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  PERFORM public.fn_log_artifact_lifecycle_event(
    v_type,
    p_artifact_id,
    'archive',
    v_actor,
    public.fn_artifact_dependency_summary(v_type, p_artifact_id)
  );

  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id);
END;
$$;


ALTER FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers'
    AS $$
DECLARE
  v_type  TEXT := lower(trim(p_artifact_type));
  v_actor UUID := lensers.get_auth_lenser_id();
BEGIN
  IF public.fn_lifecycle_is_service_role() THEN
    RETURN TRUE;
  END IF;

  IF v_actor IS NULL THEN
    RETURN FALSE;
  END IF;

  IF v_type IN ('lens', 'lenses') THEN
    RETURN EXISTS (
      SELECT 1 FROM lenses.lenses l
      WHERE l.id = p_artifact_id AND l.lenser_id = v_actor
    );
  ELSIF v_type IN ('workflow', 'workflows') THEN
    RETURN EXISTS (
      SELECT 1 FROM lenses.workflows w
      WHERE w.id = p_artifact_id AND w.lenser_id = v_actor
    );
  ELSIF v_type IN ('battle', 'battles') THEN
    RETURN EXISTS (
      SELECT 1 FROM battles.battles b
      WHERE b.id = p_artifact_id AND b.creator_lenser_id = v_actor
    );
  ELSIF v_type IN ('agent', 'ai_lenser', 'ai_lenser_id', 'lenser') THEN
    RETURN agents.can_manage_ai_lenser(p_artifact_id);
  END IF;

  RETURN FALSE;
END;
$$;


ALTER FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type TEXT := lower(trim(p_artifact_type));
BEGIN
  IF public.fn_lifecycle_is_service_role()
     OR public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RETURN TRUE;
  END IF;

  IF v_type IN ('lens', 'lenses') THEN
    RETURN EXISTS (
      SELECT 1
      FROM lenses.lenses l
      JOIN lensers.profiles p ON p.id = l.lenser_id
      WHERE l.id = p_artifact_id
        AND l.visibility IN ('public'::content.visibility_enum, 'community'::content.visibility_enum)
        AND l.status = 'published'::content.content_status
        AND l.deleted_at IS NULL
        AND p.status = 'active'::lensers.lenser_status
    );
  ELSIF v_type IN ('workflow', 'workflows') THEN
    RETURN EXISTS (
      SELECT 1
      FROM lenses.workflows w
      JOIN lensers.profiles p ON p.id = w.lenser_id
      WHERE w.id = p_artifact_id
        AND w.visibility = 'public'
        AND w.deleted_at IS NULL
        AND p.status = 'active'::lensers.lenser_status
    );
  ELSIF v_type IN ('battle', 'battles') THEN
    RETURN EXISTS (
      SELECT 1
      FROM battles.battles b
      WHERE b.id = p_artifact_id
        AND b.deleted_at IS NULL
        AND b.status IN (
          'open'::battles.battle_status_enum,
          'executing'::battles.battle_status_enum,
          'voting'::battles.battle_status_enum,
          'scoring'::battles.battle_status_enum,
          'closed'::battles.battle_status_enum,
          'published'::battles.battle_status_enum,
          'archived'::battles.battle_status_enum
        )
    );
  ELSIF v_type IN ('agent', 'ai_lenser', 'ai_lenser_id', 'lenser') THEN
    RETURN EXISTS (
      SELECT 1
      FROM agents.ai_lensers al
      WHERE al.id = p_artifact_id
        AND al.is_active = TRUE
        AND al.suspended_at IS NULL
        AND al.deleted_at IS NULL
    );
  END IF;

  RETURN FALSE;
END;
$$;


ALTER FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type       TEXT := lower(trim(p_artifact_type));
  v_actor      UUID := lensers.get_auth_lenser_id();
  v_dependency JSONB;
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_dependency := public.fn_artifact_dependency_summary(v_type, p_artifact_id);

  IF v_type = 'lens' THEN
    UPDATE lenses.lenses
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           status = 'archived'::content.content_status,
           visibility = 'private'::content.visibility_enum,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    UPDATE lenses.workflows
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           visibility = 'private',
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    UPDATE battles.battles
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           status = 'archived'::battles.battle_status_enum,
           updated_at = now()
     WHERE id = p_artifact_id;
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
      p_artifact_id,
      'archived',
      v_actor,
      jsonb_build_object('action', 'delete_tombstone', 'dependency_summary', v_dependency)
    );
  ELSIF v_type = 'agent' THEN
    UPDATE agents.ai_lensers
       SET deleted_at = COALESCE(deleted_at, now()),
           archived_at = COALESCE(archived_at, now()),
           is_active = false,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  PERFORM public.fn_log_artifact_lifecycle_event(
    v_type,
    p_artifact_id,
    'delete_tombstone',
    v_actor,
    v_dependency
  );

  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id)
    || jsonb_build_object('delete_mode', 'tombstone');
END;
$$;


ALTER FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") IS 'Dependency-aware delete request. Public APIs tombstone/withdraw governed artifacts instead of hard-deleting competitive or historical evidence.';



CREATE OR REPLACE FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'execution', 'content'
    AS $$
DECLARE
  v_type    TEXT := lower(trim(p_artifact_type));
  v_counts  JSONB := '{}'::jsonb;
  v_total   INTEGER := 0;
  v_count   INTEGER := 0;
  v_reasons TEXT[] := '{}';
BEGIN
  IF v_type IN ('lens', 'lenses') THEN
    SELECT count(*)::int INTO v_count
    FROM execution.requests r
    WHERE r.lens_id = p_artifact_id
       OR r.version_id IN (SELECT v.id FROM lenses.versions v WHERE v.lens_id = p_artifact_id);
    v_counts := v_counts || jsonb_build_object('executions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s execution requests', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.workflow_nodes WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('workflow_nodes', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s workflow nodes', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.workflow_version_nodes WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('workflow_version_nodes', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s workflow version nodes', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.battles WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battles', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battles', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.contender_lens_assignments WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battle_lens_assignments', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battle lens assignments', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.generated_challenges
    WHERE generator_lens_id = p_artifact_id
       OR generator_version_id IN (SELECT v.id FROM lenses.versions v WHERE v.lens_id = p_artifact_id);
    v_counts := v_counts || jsonb_build_object('generated_challenges', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s generated challenges', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.lens_bindings WHERE lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('agent_lens_bindings', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s agent lens bindings', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.lenses WHERE parent_lens_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('forks', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s forks', v_count); END IF;

    SELECT count(*)::int INTO v_count
    FROM lenses.versions
    WHERE lens_id = p_artifact_id AND status = 'published'::content.content_status;
    v_counts := v_counts || jsonb_build_object('published_versions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s published versions', v_count); END IF;

  ELSIF v_type IN ('workflow', 'workflows') THEN
    SELECT count(*)::int INTO v_count FROM lenses.workflow_runs WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('workflow_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s workflow runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.battles WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battles', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battles', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.submissions WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battle_submissions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battle submissions', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.team_runs WHERE workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('agent_team_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s agent team runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM lenses.workflows WHERE parent_workflow_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('forks', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s forks', v_count); END IF;

  ELSIF v_type IN ('battle', 'battles') THEN
    SELECT count(*)::int INTO v_count FROM battles.submissions WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('submissions', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s submissions', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.votes WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('votes', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s votes', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.contender_runs WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('contender_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s contender runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.battle_execution_jobs WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('execution_jobs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s execution jobs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.rule_snapshots WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('rule_snapshots', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s rule snapshots', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM battles.generated_challenges WHERE battle_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('generated_challenges', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s generated challenges', v_count); END IF;

    SELECT count(*)::int INTO v_count
    FROM battles.battles
    WHERE id = p_artifact_id AND status <> 'draft'::battles.battle_status_enum;
    v_counts := v_counts || jsonb_build_object('started_or_public_state', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || 'battle has started or been published'::text; END IF;

  ELSIF v_type IN ('agent', 'ai_lenser', 'ai_lenser_id', 'lenser') THEN
    SELECT count(*)::int INTO v_count FROM battles.contender_entity_map WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('battle_contenders', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s battle contender slots', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.team_runs WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('team_runs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s team runs', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.run_reports WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('run_reports', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s run reports', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.policy_evaluations WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('policy_evaluations', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s policy evaluations', v_count); END IF;

    SELECT count(*)::int INTO v_count FROM agents.action_logs WHERE ai_lenser_id = p_artifact_id;
    v_counts := v_counts || jsonb_build_object('action_logs', v_count);
    IF v_count > 0 THEN v_total := v_total + v_count; v_reasons := v_reasons || format('%s action logs', v_count); END IF;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  RETURN jsonb_build_object(
    'artifact_type', v_type,
    'artifact_id', p_artifact_id,
    'counts', v_counts,
    'total', v_total,
    'blocking_reasons', to_jsonb(v_reasons),
    'has_dependencies', v_total > 0,
    'can_hard_delete', v_total = 0
  );
END;
$$;


ALTER FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") IS 'Lifecycle governance dependency detector for Lens, Workflow, Battle, and Agent. Used to block unsafe hard delete/mutation and explain archive/tombstone fallback.';



CREATE OR REPLACE FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type       TEXT := lower(trim(p_artifact_type));
  v_actor      UUID := lensers.get_auth_lenser_id();
  v_dependency JSONB;
  v_pinned     BOOLEAN := FALSE;
  v_status     JSONB;
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_view(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_dependency := public.fn_artifact_dependency_summary(v_type, p_artifact_id);

  IF v_actor IS NOT NULL THEN
    SELECT EXISTS (
      SELECT 1
      FROM content.reactions r
      WHERE r.entity_type = v_type::content.entity_type_enum
        AND r.entity_id = p_artifact_id
        AND r.lenser_id = v_actor
        AND r.reaction = 'saved'::content.reaction_enum
    ) INTO v_pinned;
  END IF;

  IF v_type = 'lens' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'lens',
      'artifact_id', l.id,
      'state', CASE
        WHEN l.deleted_at IS NOT NULL THEN 'deleted'
        WHEN l.archived_at IS NOT NULL OR l.status = 'archived'::content.content_status THEN 'archived'
        ELSE l.status::text
      END,
      'visibility', l.visibility::text,
      'archived_at', l.archived_at,
      'deleted_at', l.deleted_at,
      'pinned', v_pinned,
      'version_id', l.head_version_id,
      'snapshot_hash', encode(v.content_hash, 'hex'),
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM lenses.lenses l
    LEFT JOIN lenses.versions v ON v.id = l.head_version_id
    WHERE l.id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'workflow',
      'artifact_id', w.id,
      'state', CASE
        WHEN w.deleted_at IS NOT NULL THEN 'deleted'
        WHEN w.archived_at IS NOT NULL THEN 'archived'
        ELSE 'active'
      END,
      'visibility', w.visibility,
      'archived_at', w.archived_at,
      'deleted_at', w.deleted_at,
      'pinned', v_pinned,
      'version_id', w.head_version_id,
      'snapshot_hash', wv.snapshot_hash,
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM lenses.workflows w
    LEFT JOIN lenses.workflow_versions wv ON wv.id = w.head_version_id
    WHERE w.id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'battle',
      'artifact_id', b.id,
      'state', CASE
        WHEN b.deleted_at IS NOT NULL THEN 'deleted'
        WHEN b.archived_at IS NOT NULL OR b.status = 'archived'::battles.battle_status_enum THEN 'archived'
        ELSE b.status::text
      END,
      'visibility', CASE WHEN b.deleted_at IS NULL THEN 'historical' ELSE 'tombstone' END,
      'archived_at', b.archived_at,
      'deleted_at', b.deleted_at,
      'pinned', v_pinned,
      'version_id', NULL,
      'snapshot_hash', rs.snapshot_hash,
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM battles.battles b
    LEFT JOIN LATERAL (
      SELECT snapshot_hash
      FROM battles.rule_snapshots
      WHERE battle_id = b.id
      ORDER BY created_at DESC
      LIMIT 1
    ) rs ON TRUE
    WHERE b.id = p_artifact_id;
  ELSIF v_type = 'agent' THEN
    SELECT jsonb_build_object(
      'artifact_type', 'agent',
      'artifact_id', al.id,
      'state', CASE
        WHEN al.deleted_at IS NOT NULL THEN 'deleted'
        WHEN al.archived_at IS NOT NULL THEN 'archived'
        WHEN al.is_active AND al.suspended_at IS NULL THEN 'active'
        WHEN al.suspended_at IS NOT NULL THEN 'suspended'
        ELSE 'inactive'
      END,
      'visibility', CASE WHEN al.is_active AND al.suspended_at IS NULL THEN 'public' ELSE 'restricted' END,
      'archived_at', al.archived_at,
      'deleted_at', al.deleted_at,
      'pinned', v_pinned,
      'version_id', NULL,
      'snapshot_hash', NULL,
      'dependency_summary', v_dependency
    )
    INTO v_status
    FROM agents.ai_lensers al
    WHERE al.id = p_artifact_id;
  END IF;

  IF v_status IS NULL THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  RETURN v_status;
END;
$$;


ALTER FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") IS 'Returns lifecycle state, pin state, version/snapshot ids, and dependency summary for a governed artifact.';



CREATE OR REPLACE FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean DEFAULT true) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
DECLARE
  v_actor UUID := lensers.get_auth_lenser_id();
  v_type  TEXT := lower(trim(p_artifact_type));
  v_entity content.entity_type_enum;
BEGIN
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
  END IF;

  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF v_type NOT IN ('lens', 'workflow', 'battle', 'agent') THEN
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  IF NOT public.fn_artifact_can_view(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_entity := v_type::content.entity_type_enum;

  IF p_pin THEN
    INSERT INTO content.reactions (entity_type, entity_id, lenser_id, reaction)
    VALUES (v_entity, p_artifact_id, v_actor, 'saved'::content.reaction_enum)
    ON CONFLICT (entity_type, entity_id, lenser_id, reaction) DO NOTHING;
    PERFORM public.fn_log_artifact_lifecycle_event(v_type, p_artifact_id, 'pin', v_actor, '{}'::jsonb);
  ELSE
    DELETE FROM content.reactions
    WHERE entity_type = v_entity
      AND entity_id = p_artifact_id
      AND lenser_id = v_actor
      AND reaction = 'saved'::content.reaction_enum;
    PERFORM public.fn_log_artifact_lifecycle_event(v_type, p_artifact_id, 'unpin', v_actor, '{}'::jsonb);
  END IF;

  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id);
END;
$$;


ALTER FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'battles', 'agents', 'lensers', 'content'
    AS $$
DECLARE
  v_type  TEXT := lower(trim(p_artifact_type));
  v_actor UUID := lensers.get_auth_lenser_id();
BEGIN
  IF v_type IN ('lenses') THEN v_type := 'lens'; END IF;
  IF v_type IN ('workflows') THEN v_type := 'workflow'; END IF;
  IF v_type IN ('battles') THEN v_type := 'battle'; END IF;
  IF v_type IN ('ai_lenser', 'ai_lenser_id', 'lenser') THEN v_type := 'agent'; END IF;

  IF NOT public.fn_artifact_can_manage(v_type, p_artifact_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  IF v_type = 'lens' THEN
    UPDATE lenses.lenses
       SET archived_at = NULL,
           deleted_at = NULL,
           status = CASE
             WHEN EXISTS (
               SELECT 1 FROM lenses.versions v
               WHERE v.lens_id = p_artifact_id
                 AND v.status = 'published'::content.content_status
             ) THEN 'published'::content.content_status
             ELSE 'draft'::content.content_status
           END,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'workflow' THEN
    UPDATE lenses.workflows
       SET archived_at = NULL,
           deleted_at = NULL,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSIF v_type = 'battle' THEN
    -- Restore to the most appropriate pre-archive status. Never auto-promote
    -- to 'open' — a battle that was never published (published_at IS NULL and
    -- finalized_at IS NULL) was a draft and must return to draft.
    UPDATE battles.battles
       SET archived_at = NULL,
           deleted_at = NULL,
           status = CASE
             WHEN published_at IS NOT NULL THEN 'published'::battles.battle_status_enum
             WHEN finalized_at IS NOT NULL THEN 'closed'::battles.battle_status_enum
             ELSE 'draft'::battles.battle_status_enum
           END,
           updated_at = now()
     WHERE id = p_artifact_id
       AND status = 'archived'::battles.battle_status_enum;
  ELSIF v_type = 'agent' THEN
    -- Do NOT reactivate suspended agents. A suspended agent must be explicitly
    -- unsuspended through a separate governance action. Archiving + restoring
    -- must not be a suspension bypass vector.
    UPDATE agents.ai_lensers
       SET archived_at = NULL,
           deleted_at = NULL,
           is_active = CASE WHEN suspended_at IS NULL THEN true ELSE false END,
           updated_at = now()
     WHERE id = p_artifact_id;
  ELSE
    RAISE EXCEPTION 'unsupported_artifact_type: %', p_artifact_type USING ERRCODE = '22023';
  END IF;

  PERFORM public.fn_log_artifact_lifecycle_event(v_type, p_artifact_id, 'restore', v_actor, '{}'::jsonb);
  RETURN public.fn_artifact_lifecycle_status(v_type, p_artifact_id);
END;
$$;


ALTER FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'public'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1
    FROM agents.policies ap
    WHERE ap.ai_lenser_id = p_agent_id
      AND p_modality = ANY(ap.allowed_output_modalities)
  ) THEN
    RAISE EXCEPTION 'modality_not_allowed: agent % cannot produce % output. '
      'Update agents.policies.allowed_output_modalities to unlock.', p_agent_id, p_modality
    USING ERRCODE = 'P0001';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") IS 'AP: Raises modality_not_allowed if p_modality is not in agents.policies.allowed_output_modalities for p_agent_id. Called by WorkflowExecutionService before dispatching any non-text generative node. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid", "p_input_snapshot" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("id" "uuid", "contender_id" "uuid", "battle_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "assigned_at" timestamp with time zone, "input_snapshot" "jsonb")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  INSERT INTO battles.contender_lens_assignments
    (contender_id, battle_id, lens_id, version_id, input_snapshot)
  VALUES
    (p_contender_id, p_battle_id, p_lens_id, p_version_id, p_input_snapshot)
  ON CONFLICT (contender_id) DO UPDATE
    SET lens_id        = EXCLUDED.lens_id,
        version_id     = EXCLUDED.version_id,
        input_snapshot = EXCLUDED.input_snapshot
  RETURNING id, contender_id, battle_id, lens_id, version_id, assigned_at, input_snapshot;
$$;


ALTER FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid" DEFAULT NULL::"uuid", "p_allowed" boolean DEFAULT true) RETURNS "agents"."tool_assignments"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.tool_assignments;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.tool_assignments (ai_lenser_id, tool_id, profile_id, allowed)
  VALUES (p_ai_lenser_id, p_tool_id, p_profile_id, COALESCE(p_allowed, true))
  ON CONFLICT (ai_lenser_id, tool_id)
  DO UPDATE SET
    profile_id = EXCLUDED.profile_id,
    allowed    = EXCLUDED.allowed
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_approve_device_request(p_user_code => p_user_code);
END;
$$;


ALTER FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_exchange_device_approval(
    p_request_id => p_request_id,
    p_request_secret => p_request_secret
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_exchange_device_login(
    p_request_id     => p_request_id,
    p_request_secret => p_request_secret
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_list_developer_tokens"() RETURNS TABLE("id" "uuid", "label" "text", "tokenPrefix" "text", "status" "text", "expiresAt" timestamp with time zone, "createdAt" timestamp with time zone, "revokedAt" timestamp with time zone, "lastUsedAt" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM authz.fn_list_developer_tokens();
END;
$$;


ALTER FUNCTION "public"."fn_auth_list_developer_tokens"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text" DEFAULT NULL::"text", "p_request_ttl_minutes" integer DEFAULT 10, "p_token_ttl_hours" integer DEFAULT 24) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_request_device_approval(
    p_label => p_label,
    p_request_ttl_minutes => p_request_ttl_minutes,
    p_token_ttl_hours => p_token_ttl_hours
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer DEFAULT 10) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_request_device_login(
    p_request_ttl_minutes => p_request_ttl_minutes
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  PERFORM authz.fn_revoke_developer_token(p_token_id => p_token_id);
END;
$$;


ALTER FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'authz', 'public'
    AS $$
BEGIN
  RETURN authz.fn_store_device_login_session(
    p_user_code     => p_user_code,
    p_access_token  => p_access_token,
    p_refresh_token => p_refresh_token
  );
END;
$$;


ALTER FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean DEFAULT false) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'agents'
    AS $_$
DECLARE
  v_caller          uuid := lensers.get_auth_lenser_id();
  v_schedule        record;
  v_parts           text[];
  v_minute          text;
  v_hour            text;
  v_dom             text;
  v_mon             text;
  v_dow             text;
  v_step_minutes    int;
  v_target_hour     int;
  v_target_dow      int;
  v_tick            timestamptz;
  v_dispatched      int := 0;
  v_skipped         int := 0;
  v_ticks           jsonb := '[]'::jsonb;
  v_backfill_key    text;
  v_run_id          uuid;
  v_now             timestamptz := date_trunc('minute', now());
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  IF p_since IS NULL OR p_since >= now() THEN
    RAISE EXCEPTION 'p_since must be in the past' USING ERRCODE = '22023';
  END IF;

  -- Cap window at 30 days to bound cost.
  IF p_since < now() - interval '30 days' THEN
    RAISE EXCEPTION 'p_since exceeds 30-day backfill window' USING ERRCODE = '22023';
  END IF;

  SELECT s.id,
         s.workflow_id,
         s.cron_expr,
         s.inputs_template,
         s.global_model_id,
         w.lenser_id   AS owner_id
  INTO   v_schedule
  FROM   lenses.workflow_schedules s
  JOIN   lenses.workflows w ON w.id = s.workflow_id
  WHERE  s.id = p_schedule_id;

  IF v_schedule.id IS NULL THEN
    RAISE EXCEPTION 'schedule_not_found: %', p_schedule_id USING ERRCODE = 'P0002';
  END IF;

  IF v_schedule.owner_id IS DISTINCT FROM v_caller THEN
    RAISE EXCEPTION 'not_workflow_owner' USING ERRCODE = '42501';
  END IF;

  -- ── Parse the cron expression ──────────────────────────────────────────
  v_parts := regexp_split_to_array(trim(v_schedule.cron_expr), '\s+');
  IF array_length(v_parts, 1) <> 5 THEN
    RAISE EXCEPTION
      'unsupported_cron_expression: % (expected 5 fields)',
      v_schedule.cron_expr
      USING ERRCODE = '22023';
  END IF;

  v_minute := v_parts[1];
  v_hour   := v_parts[2];
  v_dom    := v_parts[3];
  v_mon    := v_parts[4];
  v_dow    := v_parts[5];

  -- Build the tick stream. Truncate p_since to minute precision.
  IF v_minute = '*' AND v_hour = '*' AND v_dom = '*' AND v_mon = '*' AND v_dow = '*' THEN
    -- every minute
    FOR v_tick IN
      SELECT g
      FROM generate_series(date_trunc('minute', p_since), v_now, interval '1 minute') g
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSIF v_minute LIKE '*/%' AND v_hour = '*' AND v_dom = '*' AND v_mon = '*' AND v_dow = '*' THEN
    -- every N minutes
    BEGIN
      v_step_minutes := substr(v_minute, 3)::int;
    EXCEPTION WHEN OTHERS THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END;

    IF v_step_minutes <= 0 OR v_step_minutes > 59 THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END IF;

    FOR v_tick IN
      SELECT g
      FROM generate_series(date_trunc('hour', p_since), v_now, (v_step_minutes::text || ' minutes')::interval) g
      WHERE g >= p_since
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSIF v_minute = '0' AND v_dom = '*' AND v_mon = '*' AND v_dow = '*'
        AND v_hour ~ '^[0-9]+$' THEN
    -- 0 H * * *  (daily at hour H, UTC)
    v_target_hour := v_hour::int;
    IF v_target_hour < 0 OR v_target_hour > 23 THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END IF;

    FOR v_tick IN
      SELECT g
      FROM generate_series(
        date_trunc('day', p_since AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
          + (v_target_hour::text || ' hours')::interval,
        v_now,
        interval '1 day'
      ) g
      WHERE g >= p_since AND g <= v_now
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSIF v_minute = '0' AND v_dom = '*' AND v_mon = '*'
        AND v_hour ~ '^[0-9]+$' AND v_dow ~ '^[0-6]$' THEN
    -- 0 H * * DOW  (weekly, hour H, day-of-week DOW; 0=Sun)
    v_target_hour := v_hour::int;
    v_target_dow  := v_dow::int;
    IF v_target_hour < 0 OR v_target_hour > 23 THEN
      RAISE EXCEPTION 'unsupported_cron_expression: %', v_schedule.cron_expr USING ERRCODE = '22023';
    END IF;

    FOR v_tick IN
      SELECT g
      FROM generate_series(
        date_trunc('day', p_since AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
          + (v_target_hour::text || ' hours')::interval,
        v_now,
        interval '1 day'
      ) g
      WHERE g >= p_since AND g <= v_now
        AND extract(dow from g)::int = v_target_dow
    LOOP
      v_ticks := v_ticks || to_jsonb(v_tick);
    END LOOP;

  ELSE
    RAISE EXCEPTION
      'unsupported_cron_expression: % (supported: ''*/N * * * *'', ''0 H * * *'', ''0 H * * DOW'')',
      v_schedule.cron_expr
      USING ERRCODE = '22023';
  END IF;

  -- ── Dry-run path ───────────────────────────────────────────────────────
  IF p_dry_run THEN
    RETURN jsonb_build_object(
      'would_dispatch', jsonb_array_length(v_ticks),
      'ticks',          v_ticks
    );
  END IF;

  -- ── Execute path ───────────────────────────────────────────────────────
  FOR v_tick IN
    SELECT (value::text)::timestamptz FROM jsonb_array_elements_text(v_ticks)
  LOOP
    v_backfill_key := p_schedule_id::text || ':' ||
                      to_char(v_tick AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"');

    -- Skip if a run for this exact slot already exists (either via
    -- backfill_id metadata key or via the existing schedule_id+scheduled_for
    -- unique slot index).
    IF EXISTS (
      SELECT 1 FROM lenses.workflow_runs
      WHERE metadata->>'backfill_id' = v_backfill_key
    ) OR EXISTS (
      SELECT 1 FROM lenses.workflow_runs
      WHERE schedule_id = p_schedule_id
        AND scheduled_for = v_tick
    ) THEN
      v_skipped := v_skipped + 1;
      CONTINUE;
    END IF;

    INSERT INTO lenses.workflow_runs (
      workflow_id,
      triggered_by,
      status,
      context_inputs,
      global_model_id,
      schedule_id,
      scheduled_for,
      trigger_mode,
      metadata
    ) VALUES (
      v_schedule.workflow_id,
      v_caller,
      'pending',
      COALESCE(v_schedule.inputs_template, '{}'::jsonb),
      v_schedule.global_model_id,
      p_schedule_id,
      v_tick,
      'schedule',
      jsonb_build_object('backfill_id', v_backfill_key, 'origin', 'backfill')
    )
    RETURNING id INTO v_run_id;

    -- Materialise node placeholders so the worker can pick this run up.
    INSERT INTO lenses.workflow_node_results (run_id, node_id, status)
    SELECT v_run_id, n.id, 'pending'
    FROM   lenses.workflow_nodes n
    WHERE  n.workflow_id = v_schedule.workflow_id;

    v_dispatched := v_dispatched + 1;
  END LOOP;

  RETURN jsonb_build_object(
    'dispatched',       v_dispatched,
    'skipped_existing', v_skipped
  );
END;
$_$;


ALTER FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) IS 'Phase Q1: enumerates missed cron ticks between p_since and now() for a workflow schedule the caller owns and inserts pending workflow_runs the recovery sweeper will pick up. Supports * * * * *, */N * * * *, 0 H * * *, 0 H * * DOW. Dedupes via metadata.backfill_id and (schedule_id, scheduled_for). Returns jsonb with dispatched/skipped or would_dispatch/ticks.';



CREATE OR REPLACE FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'xp', 'public'
    AS $$
DECLARE
  v_total_xp   BIGINT;
  v_level      INT;
  v_streak     INT;
  v_forum_app  CONSTANT uuid := '00000000-0000-0000-0000-000000000001';
BEGIN
  -- Fetch current XP + level
  SELECT total_xp, current_level
    INTO v_total_xp, v_level
    FROM xp.totals
   WHERE lenser_id = p_lenser_id
     AND app_id    = v_forum_app;

  IF NOT FOUND THEN
    RETURN;
  END IF;

  -- Fetch daily streak
  SELECT current_streak
    INTO v_streak
    FROM xp.streaks
   WHERE lenser_id   = p_lenser_id
     AND streak_type = 'daily';

  -- ---- Level milestone badges -------------------------------------------
  -- Newcomer  (level 5)
  IF v_level >= 5 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_5', 'Newcomer', 'Reached Level 5', '🌱')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Builder (level 10)
  IF v_level >= 10 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_10', 'Builder', 'Reached Level 10', '🏗️')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Expert (level 25)
  IF v_level >= 25 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_25', 'Expert', 'Reached Level 25', '⚡')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Architect (level 50)
  IF v_level >= 50 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_50', 'Architect', 'Reached Level 50', '🏛️')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Legend (level 75)
  IF v_level >= 75 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_75', 'Legend', 'Reached Level 75', '🌟')
    ON CONFLICT DO NOTHING;
  END IF;

  -- Champion (level 100)
  IF v_level >= 100 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'level_milestone_100', 'Champion', 'Reached Level 100 — all-time elite', '🏆')
    ON CONFLICT DO NOTHING;
  END IF;

  -- ---- XP milestone badges -----------------------------------------------
  IF v_total_xp >= 1000 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'xp_1k', '1K XP', 'Earned 1,000 total XP', '💡')
    ON CONFLICT DO NOTHING;
  END IF;

  IF v_total_xp >= 10000 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'xp_10k', '10K XP', 'Earned 10,000 total XP', '🔥')
    ON CONFLICT DO NOTHING;
  END IF;

  IF v_total_xp >= 50000 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'xp_50k', '50K XP', 'Earned 50,000 total XP', '💎')
    ON CONFLICT DO NOTHING;
  END IF;

  -- ---- Streak milestone badges -------------------------------------------
  IF v_streak >= 7 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'streak_7d', '7-Day Streak', 'Logged in 7 days in a row', '🔆')
    ON CONFLICT DO NOTHING;
  END IF;

  IF v_streak >= 30 THEN
    INSERT INTO lensers.badges (lenser_id, type, label, description, icon)
    VALUES (p_lenser_id, 'streak_30d', '30-Day Streak', 'Logged in 30 days in a row', '☀️')
    ON CONFLICT DO NOTHING;
  END IF;

EXCEPTION WHEN OTHERS THEN
  -- Non-fatal: badge award errors must not roll back the parent XP transaction
  RAISE NOTICE 'fn_badge_check_and_award: % (lenser=%)', SQLERRM, p_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") IS 'CB: Awards milestone badges based on XP totals, level, and streak. SECURITY DEFINER. Non-fatal on error.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_creator_id uuid;
BEGIN
  SELECT id INTO v_creator_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_creator_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated.';
  END IF;

  UPDATE battles.battles
  SET
    status           = 'scoring',
    voting_closes_at = now(),
    updated_at       = now()
  WHERE id                = p_battle_id
    AND creator_lenser_id = v_creator_id
    AND status            = 'voting'
    AND deleted_at        IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Battle not found, not owned by you, or not in voting status.';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") IS 'Transitions a battle from voting to scoring. Sets voting_closes_at = now(). Only callable by the battle creator. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'execution', 'public'
    AS $$
DECLARE
  v_lenser_id  UUID;
  v_battle     battles.battles%ROWTYPE;
  v_contender  battles.contenders%ROWTYPE;
BEGIN
  -- Resolve caller lenser
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  -- Verify agent ownership
  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    WHERE  al.id = p_agent_id AND al.profile_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'dispatch_agent_forbidden: agent not owned by caller';
  END IF;

  -- Verify battle is in a submittable state
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'dispatch_agent_battle_not_found: %', p_battle_id;
  END IF;
  IF v_battle.status NOT IN ('open', 'executing') THEN
    RAISE EXCEPTION 'dispatch_agent_battle_not_open: status=%', v_battle.status;
  END IF;

  -- Verify contender registration
  SELECT c.* INTO v_contender
  FROM   battles.contenders c
  JOIN   lensers.profiles p ON p.id = c.lenser_id
  WHERE  c.battle_id = p_battle_id
  AND    p.id = v_lenser_id
  LIMIT  1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'dispatch_agent_not_registered: join the battle first';
  END IF;

  -- Log dispatch intent
  INSERT INTO audit.events (event_type, actor_type, actor_id, payload)
  VALUES (
    'battle.agent_dispatch_queued',
    'lenser',
    v_lenser_id,
    jsonb_build_object(
      'battle_id',   p_battle_id,
      'agent_id',    p_agent_id,
      'model_spec',  p_model_spec,
      'device_id',   p_device_id,
      'workflow_id', p_workflow_id
    )
  );

  RETURN jsonb_build_object(
    'battle_id',    p_battle_id,
    'agent_id',     p_agent_id,
    'contender_id', v_contender.id,
    'model_spec',   p_model_spec,
    'status',       'queued'
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'audit', 'public'
    AS $$
DECLARE
  v_operator_id UUID;
  v_old_status  TEXT;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'force_transition_forbidden: admin role required';
  END IF;

  SELECT p.id INTO v_operator_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  SELECT status::TEXT INTO v_old_status
  FROM   battles.battles
  WHERE  id = p_battle_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'force_transition_battle_not_found: %', p_battle_id;
  END IF;

  IF p_target_status NOT IN ('draft', 'open', 'executing', 'voting', 'scoring', 'closed', 'published', 'archived') THEN
    RAISE EXCEPTION 'force_transition_invalid_status: %', p_target_status;
  END IF;

  UPDATE battles.battles
  SET    status       = p_target_status::battles.battle_status_enum,
         updated_at   = now(),
         published_at = CASE WHEN p_target_status = 'published' THEN now() ELSE published_at END,
         finalized_at = CASE WHEN p_target_status IN ('closed', 'published') THEN now() ELSE finalized_at END
  WHERE  id = p_battle_id;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'battle.admin.force_transition',
    'lenser',
    v_operator_id,
    'warn',
    jsonb_build_object(
      'battle_id',     p_battle_id,
      'from_status',   v_old_status,
      'to_status',     p_target_status,
      'reason',        p_reason
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean DEFAULT false) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_link  battles.invite_links%ROWTYPE;
BEGIN
  SELECT * INTO v_link
    FROM battles.invite_links
   WHERE token = p_token
   FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'invite_token_not_found' USING ERRCODE = 'P0001';
  END IF;

  IF v_link.expires_at IS NOT NULL AND v_link.expires_at < NOW() THEN
    RAISE EXCEPTION 'invite_token_expired' USING ERRCODE = 'P0001';
  END IF;

  -- Increment the right counter.
  IF p_via_qr OR v_link.type = 'qr' THEN
    UPDATE battles.invite_links
       SET qr_scan_count = qr_scan_count + 1,
           accepted_at   = COALESCE(accepted_at, NOW())
     WHERE id = v_link.id;
  ELSE
    UPDATE battles.invite_links
       SET click_count  = click_count + 1,
           accepted_at  = COALESCE(accepted_at, NOW())
     WHERE id = v_link.id;
  END IF;

  RETURN jsonb_build_object(
    'battle_id',  v_link.battle_id,
    'type',       v_link.type,
    'invite_url', v_link.invite_url
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) IS 'Records a click or QR scan on a battle invite token. Granted to anon so it works before the visitor has an account.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text" DEFAULT 'public'::"text", "p_target_handle" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller      UUID;
  v_link_type   battles.invite_link_type;
  v_token       TEXT;
  v_url         TEXT;
  v_id          UUID;
  v_base_url    TEXT := 'https://lenserfight.com';
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  -- Validate caller owns the battle.
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
     WHERE b.id = p_battle_id
       AND b.creator_lenser_id = v_caller
  ) THEN
    RAISE EXCEPTION 'battle_not_found_or_not_owner' USING ERRCODE = 'P0001';
  END IF;

  -- Validate type.
  BEGIN
    v_link_type := p_type::battles.invite_link_type;
  EXCEPTION WHEN invalid_text_representation THEN
    RAISE EXCEPTION 'invalid_invite_type: %', p_type USING ERRCODE = 'P0001';
  END;

  -- Private requires target_handle.
  IF v_link_type = 'private' AND p_target_handle IS NULL THEN
    RAISE EXCEPTION 'private_invite_requires_target_handle' USING ERRCODE = 'P0001';
  END IF;

  -- Generate token and URL.
  v_token := encode(extensions.gen_random_bytes(10), 'hex');

  -- Build battle slug from battle id (slug stored on battle or fallback to id).
  SELECT COALESCE(
    (SELECT b.slug FROM battles.battles b WHERE b.id = p_battle_id),
    p_battle_id::TEXT
  ) INTO v_url;
  v_url := v_base_url || '/b/' || v_url || '?ref=' || v_token;

  INSERT INTO battles.invite_links (
    battle_id, created_by, type, token, invite_url, target_handle
  ) VALUES (
    p_battle_id, v_caller, v_link_type, v_token, v_url, p_target_handle
  )
  RETURNING id INTO v_id;

  RETURN jsonb_build_object(
    'id',         v_id,
    'token',      v_token,
    'invite_url', v_url,
    'type',       p_type,
    'battle_id',  p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") IS 'Creates a battle invite link (public/private/link/qr). Caller must own the battle. Returns id, token, and invite_url.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer DEFAULT 50) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller UUID;
  v_rows   JSONB;
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
     WHERE b.id = p_battle_id
       AND b.creator_lenser_id = v_caller
  ) THEN
    RAISE EXCEPTION 'battle_not_found_or_not_owner' USING ERRCODE = 'P0001';
  END IF;

  SELECT jsonb_agg(
    jsonb_build_object(
      'id',             il.id,
      'type',           il.type,
      'token',          il.token,
      'invite_url',     il.invite_url,
      'target_handle',  il.target_handle,
      'click_count',    il.click_count,
      'qr_scan_count',  il.qr_scan_count,
      'accepted_at',    il.accepted_at,
      'created_at',     il.created_at,
      'expires_at',     il.expires_at
    )
    ORDER BY il.created_at DESC
  )
  INTO v_rows
  FROM (
    SELECT * FROM battles.invite_links
     WHERE battle_id = p_battle_id
     LIMIT LEAST(p_limit, 200)
  ) il;

  RETURN COALESCE(v_rows, '[]'::JSONB);
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) IS 'Lists invite links for a battle (up to 200). Caller must own the battle.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller UUID;
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  -- Only the battle owner can view stats.
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
     WHERE b.id = p_battle_id
       AND b.creator_lenser_id = v_caller
  ) THEN
    RAISE EXCEPTION 'battle_not_found_or_not_owner' USING ERRCODE = 'P0001';
  END IF;

  RETURN (
    SELECT jsonb_build_object(
      'battle_id',    p_battle_id,
      'link_clicks',  COALESCE(SUM(click_count)   FILTER (WHERE type IN ('public','link')), 0),
      'qr_scans',     COALESCE(SUM(qr_scan_count) FILTER (WHERE type = 'qr'),              0),
      'accepted',     COUNT(*) FILTER (WHERE accepted_at IS NOT NULL),
      'total_links',  COUNT(*)
    )
    FROM battles.invite_links
   WHERE battle_id = p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") IS 'Returns aggregated click, scan, accepted, and total-link counts for a battle. Caller must own the battle.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_creator_id     uuid;
  v_accepted_count int;
BEGIN
  SELECT id INTO v_creator_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_creator_id IS NULL THEN
    RAISE EXCEPTION 'Not authenticated.';
  END IF;

  -- Require at least 2 accepted contenders before voting can open
  SELECT COUNT(*) INTO v_accepted_count
  FROM battles.contenders
  WHERE battle_id        = p_battle_id
    AND contender_status = 'accepted';

  IF v_accepted_count < 2 THEN
    RAISE EXCEPTION 'Battle must have at least 2 accepted contenders before voting can open.';
  END IF;

  UPDATE battles.battles
  SET
    status          = 'voting',
    voting_opens_at = now(),
    updated_at      = now()
  WHERE id                = p_battle_id
    AND creator_lenser_id = v_creator_id
    AND status            = 'open'
    AND deleted_at        IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Battle not found, not owned by you, or not in open status.';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") IS 'Transitions a battle from open to voting. Requires at least 2 accepted contenders. Sets voting_opens_at = now(). Only callable by the battle creator. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_judge_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_publish_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_auto_judge" boolean DEFAULT true, "p_auto_publish" boolean DEFAULT true) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_sched_id  UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid();

  IF NOT EXISTS (
    SELECT 1 FROM battles.battles
    WHERE id = p_battle_id AND creator_lenser_id = v_lenser_id
  ) AND NOT public.is_admin() THEN
    RAISE EXCEPTION 'set_schedule_forbidden: must be battle creator or admin';
  END IF;

  INSERT INTO battles.schedules (battle_id, open_at, judge_at, publish_at, auto_judge, auto_publish)
  VALUES (p_battle_id, p_open_at, p_judge_at, p_publish_at, p_auto_judge, p_auto_publish)
  ON CONFLICT (battle_id) DO UPDATE
    SET open_at      = EXCLUDED.open_at,
        judge_at     = EXCLUDED.judge_at,
        publish_at   = EXCLUDED.publish_at,
        auto_judge   = EXCLUDED.auto_judge,
        auto_publish = EXCLUDED.auto_publish,
        updated_at   = now()
  RETURNING id INTO v_sched_id;

  RETURN v_sched_id;
END;
$$;


ALTER FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid" DEFAULT NULL::"uuid", "p_agent_id" "uuid" DEFAULT NULL::"uuid", "p_content" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id    UUID;
  v_submission_id UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  -- Verify contender registration
  IF NOT EXISTS (
    SELECT 1 FROM battles.contenders c
    WHERE  c.battle_id  = p_battle_id
    AND    c.lenser_id  = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'submit_workflow_not_registered: join battle first';
  END IF;

  -- Verify workflow ownership or visibility
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE  w.id = p_workflow_id
    AND    (w.lenser_id = v_lenser_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'submit_workflow_not_accessible: workflow % not found or not accessible', p_workflow_id;
  END IF;

  -- Get contender_id
  INSERT INTO battles.submissions (
    battle_id,
    contender_id,
    submission_type,
    workflow_id,
    execution_run_id,
    source_type,
    content_text,
    status
  )
  SELECT
    p_battle_id,
    c.id,
    'workflow',
    p_workflow_id,
    p_run_id,
    CASE WHEN p_run_id IS NOT NULL THEN 'execution_output' ELSE 'manual' END,
    p_content,
    CASE WHEN p_content IS NOT NULL THEN 'submitted' ELSE 'pending' END
  FROM battles.contenders c
  WHERE c.battle_id = p_battle_id
  AND   c.lenser_id = v_lenser_id
  LIMIT 1
  RETURNING id INTO v_submission_id;

  RETURN v_submission_id;
END;
$$;


ALTER FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid" DEFAULT NULL::"uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  UPDATE battles.submissions s
  SET    content_text      = p_content,
         execution_run_id  = COALESCE(p_execution_run_id, s.execution_run_id),
         status            = 'submitted',
         submitted_at      = now(),
         updated_at        = now()
  FROM   battles.contenders c
  WHERE  s.id         = p_submission_id
  AND    c.id         = s.contender_id
  AND    c.lenser_id  = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'update_workflow_submission_forbidden_or_not_found: %', p_submission_id;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_archive('battle', p_battle_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'public'
    AS $$
DECLARE
  v_readiness JSONB;
  v_battle    RECORD;
BEGIN
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id AND status = 'draft';
  IF NOT FOUND THEN
    RETURN FALSE;
  END IF;

  v_readiness := public.fn_battles_check_readiness(p_battle_id);

  IF NOT (v_readiness ->> 'ready')::BOOLEAN THEN
    RETURN FALSE;
  END IF;

  UPDATE battles.battles
     SET status = 'open',
         updated_at = now()
   WHERE id = p_battle_id
     AND status = 'draft';

  IF FOUND THEN
    INSERT INTO audit.events (actor_id, event_type, resource_type, resource_id, meta)
    VALUES (
      NULL,
      'battle.auto_promoted',
      'battle',
      p_battle_id,
      jsonb_build_object('readiness', v_readiness)
    );
    RETURN TRUE;
  END IF;

  RETURN FALSE;
END;
$$;


ALTER FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") IS 'CB: Promotes draft→open when readiness check passes. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_max_contenders INT;
  v_existing_count INT;
  v_to_assign      INT;
  v_assigned       INT := 0;
  rec              RECORD;
BEGIN
  -- Read max_contenders; default to 2
  SELECT COALESCE(max_contenders, 2)
    INTO v_max_contenders
    FROM battles.battles
   WHERE id = p_battle_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Battle not found' USING ERRCODE = 'P0002';
  END IF;

  SELECT COUNT(*)
    INTO v_existing_count
    FROM battles.contenders
   WHERE battle_id = p_battle_id;

  v_to_assign := v_max_contenders - v_existing_count;
  IF v_to_assign <= 0 THEN
    RETURN 0;
  END IF;

  FOR rec IN
    SELECT al.id AS lenser_id
      FROM lensers.ai_lensers al
     WHERE al.is_available = TRUE
       AND NOT EXISTS (
         SELECT 1 FROM battles.contenders c
          WHERE c.battle_id = p_battle_id
            AND c.lenser_id = al.id
       )
     ORDER BY al.last_active_at DESC NULLS LAST
     LIMIT v_to_assign
  LOOP
    INSERT INTO battles.contenders (battle_id, lenser_id, slot, status)
    VALUES (p_battle_id, rec.lenser_id,
      CASE WHEN v_assigned = 0 THEN 'A' ELSE 'B' END,
      'pending')
    ON CONFLICT DO NOTHING;
    v_assigned := v_assigned + 1;
  END LOOP;

  RETURN v_assigned;
END;
$$;


ALTER FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") IS 'CB: Assigns available AI lensers up to max_contenders for auto-assign battles.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_voter      UUID;
  v_status     TEXT;
  v_existing   battles.votes%ROWTYPE;
  v_old_cid    UUID;
BEGIN
  v_voter := lensers.get_auth_lenser_id();
  IF v_voter IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  SELECT status::text INTO v_status
    FROM battles.battles WHERE id = p_battle_id;

  IF v_status IS NULL THEN
    RAISE EXCEPTION 'battle_not_found' USING ERRCODE = '42501';
  END IF;
  IF v_status <> 'voting' THEN
    RAISE EXCEPTION 'battle_closed: status=%', v_status USING ERRCODE = 'P0001';
  END IF;

  -- contender must belong to this battle
  IF NOT EXISTS (
    SELECT 1 FROM battles.contenders
     WHERE id = p_new_contender_id AND battle_id = p_battle_id
  ) THEN
    RAISE EXCEPTION 'contender_not_in_battle' USING ERRCODE = '22023';
  END IF;

  SELECT * INTO v_existing
    FROM battles.votes
   WHERE battle_id = p_battle_id
     AND voter_lenser_id = v_voter
   FOR UPDATE;

  IF v_existing.id IS NULL THEN
    -- First vote — just delegate to fn_submit_vote semantics inline.
    INSERT INTO battles.votes (
      battle_id, voter_lenser_id, vote_value, voted_contender_id, is_draw, rationale
    )
    VALUES (
      p_battle_id, v_voter, 'contender_a'::battles.vote_value_enum,
      p_new_contender_id, false, NULL
    )
    RETURNING * INTO v_existing;

    INSERT INTO battles.vote_aggregates (
      battle_id, contender_id, raw_vote_count, weighted_vote_sum, draw_count
    ) VALUES (
      p_battle_id, p_new_contender_id, 1, 1, 0
    )
    ON CONFLICT (battle_id, contender_id) DO UPDATE SET
      raw_vote_count    = battles.vote_aggregates.raw_vote_count + 1,
      weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1,
      updated_at        = now();

    UPDATE battles.battles
       SET total_vote_count = total_vote_count + 1
     WHERE id = p_battle_id;

    RETURN jsonb_build_object(
      'vote_id',    v_existing.id,
      'updated_at', v_existing.updated_at,
      'status',     'created'
    );
  END IF;

  v_old_cid := v_existing.voted_contender_id;
  IF v_old_cid IS NOT DISTINCT FROM p_new_contender_id THEN
    -- No-op; touch updated_at so callers can see the action.
    UPDATE battles.votes
       SET updated_at = now()
     WHERE id = v_existing.id
     RETURNING updated_at INTO v_existing.updated_at;

    RETURN jsonb_build_object(
      'vote_id',    v_existing.id,
      'updated_at', v_existing.updated_at,
      'status',     'unchanged'
    );
  END IF;

  -- Swap: decrement the old aggregate, increment the new one, update the row.
  UPDATE battles.vote_aggregates
     SET raw_vote_count    = GREATEST(raw_vote_count    - 1, 0),
         weighted_vote_sum = GREATEST(weighted_vote_sum - 1, 0),
         updated_at        = now()
   WHERE battle_id = p_battle_id
     AND contender_id = v_old_cid;

  INSERT INTO battles.vote_aggregates (
    battle_id, contender_id, raw_vote_count, weighted_vote_sum, draw_count
  ) VALUES (
    p_battle_id, p_new_contender_id, 1, 1, 0
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE SET
    raw_vote_count    = battles.vote_aggregates.raw_vote_count + 1,
    weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1,
    updated_at        = now();

  UPDATE battles.votes
     SET voted_contender_id = p_new_contender_id
   WHERE id = v_existing.id
   RETURNING * INTO v_existing;

  RETURN jsonb_build_object(
    'vote_id',    v_existing.id,
    'updated_at', v_existing.updated_at,
    'status',     'changed'
  );
END $$;


ALTER FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_battle         RECORD;
  v_contender_cnt  INT;
  v_blockers       TEXT[] := '{}';
  v_render_ok      BOOLEAN := TRUE;
  v_byok_ok        BOOLEAN := TRUE;
  v_byok_result    JSONB;
  rec              RECORD;
BEGIN
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    RETURN jsonb_build_object('ready', false, 'blockers', ARRAY['battle_not_found']);
  END IF;

  -- Check template valid (template_id set)
  IF v_battle.template_id IS NULL AND (v_battle.task_prompt IS NULL OR v_battle.task_prompt = '') THEN
    v_blockers := v_blockers || ARRAY['no_template_or_task_prompt'];
  END IF;

  -- Check render prompt succeeds (only if template_id set)
  IF v_battle.template_id IS NOT NULL THEN
    BEGIN
      PERFORM public.fn_battles_render_prompt(p_battle_id, '{}'::JSONB);
    EXCEPTION WHEN OTHERS THEN
      v_render_ok := FALSE;
      v_blockers  := v_blockers || ARRAY[CONCAT('prompt_render_failed:', SQLERRM)];
    END;
  END IF;

  -- Check ≥2 contenders assigned
  SELECT COUNT(*) INTO v_contender_cnt
    FROM battles.contenders
   WHERE battle_id = p_battle_id;

  IF v_contender_cnt < 2 THEN
    v_blockers := v_blockers || ARRAY[CONCAT('insufficient_contenders:', v_contender_cnt::TEXT)];
  END IF;

  -- Check BYOK valid for AI contenders
  FOR rec IN
    SELECT c.id AS contender_id
      FROM battles.contenders c
     WHERE c.battle_id = p_battle_id
       AND c.contender_type IN ('ai_model', 'ai_agent')
  LOOP
    v_byok_result := public.fn_byok_validate_for_battle(p_battle_id, rec.contender_id);
    IF NOT (v_byok_result ->> 'valid')::BOOLEAN THEN
      v_blockers := v_blockers || ARRAY[
        CONCAT('byok_invalid:contender=', rec.contender_id,
               ':reason=', v_byok_result ->> 'reason')
      ];
      v_byok_ok := FALSE;
    END IF;
  END LOOP;

  RETURN jsonb_build_object(
    'ready',    array_length(v_blockers, 1) IS NULL,
    'blockers', v_blockers
  );
END;
$$;


ALTER FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") IS 'CB: Returns {ready, blockers[]} for a battle. Used before auto-promote.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_source RECORD;
    v_new_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_source
    FROM battles.battles
    WHERE id = p_battle_id
      AND deleted_at IS NULL;

    IF v_source IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    INSERT INTO battles.battles (
        creator_lenser_id, title, slug, task_prompt,
        rubric_id, status, max_contenders
    )
    VALUES (
        v_lenser_id, p_title, p_slug, v_source.task_prompt,
        v_source.rubric_id, 'draft', v_source.max_contenders
    )
    RETURNING id INTO v_new_id;

    -- Log the event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        v_new_id,
        'status_change',
        v_lenser_id,
        jsonb_build_object('action', 'cloned', 'source_battle_id', p_battle_id)
    );

    RETURN v_new_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_lenser_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id != v_lenser_id THEN
        RAISE EXCEPTION 'Only the battle creator can close';
    END IF;

    IF v_battle.status NOT IN ('open', 'scoring') THEN
        RAISE EXCEPTION 'Battle must be in open or scoring status to close (current: %)', v_battle.status;
    END IF;

    UPDATE battles.battles
    SET status = 'closed',
        finalized_at = now(),
        updated_at = now()
    WHERE id = p_battle_id;

    -- Log the event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        p_battle_id,
        'status_change',
        v_lenser_id,
        jsonb_build_object('from', v_battle.status::text, 'to', 'closed')
    );
END;
$$;


ALTER FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id   uuid;
  v_battle_id   uuid;
  v_count       integer;
  v_oldest_at   timestamptz;
  v_retry_after integer;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT count(*), min(created_at)
    INTO v_count, v_oldest_at
    FROM battles.battles
   WHERE creator_lenser_id = v_lenser_id
     AND created_at > now() - interval '1 hour';

  IF v_count >= 20 THEN
    v_retry_after := GREATEST(0, EXTRACT(epoch FROM (v_oldest_at + interval '1 hour' - now()))::integer);
    RAISE EXCEPTION 'battle_rate_limit_exceeded'
      USING HINT   = 'p0429',
            DETAIL = '{"retry_after":' || v_retry_after || '}';
  END IF;

  INSERT INTO battles.battles (creator_lenser_id, title, slug, task_prompt, rubric_id, status)
  VALUES (v_lenser_id, p_title, p_slug, p_task_prompt, p_rubric_id, 'draft')
  RETURNING id INTO v_battle_id;

  RETURN v_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_template RECORD;
  v_lenser_id uuid;
  v_battle_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();

  SELECT * INTO v_template
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL
     AND (is_public = true OR creator_lenser_id = v_lenser_id);

  IF v_template IS NULL THEN
    RAISE EXCEPTION 'Template not found or not accessible';
  END IF;

  INSERT INTO battles.battles (
    creator_lenser_id, title, slug, task_prompt, rubric_id,
    status, max_contenders, template_id
  )
  VALUES (
    v_lenser_id, p_title, p_slug, v_template.task_prompt,
    v_template.rubric_id, 'draft', v_template.max_contenders, p_template_id
  )
  RETURNING id INTO v_battle_id;

  RETURN v_battle_id;
END $$;


ALTER FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller   uuid;
  v_owner    uuid;
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
  END IF;

  SELECT creator_lenser_id INTO v_owner
    FROM battles.battles
   WHERE id = p_parent_id
     AND deleted_at IS NULL;

  IF v_owner IS NULL THEN
    RAISE EXCEPTION 'parent_battle_not_found: %', p_parent_id
      USING ERRCODE = 'P0002';
  END IF;

  IF v_owner IS DISTINCT FROM v_caller THEN
    RAISE EXCEPTION 'not_battle_owner' USING ERRCODE = '42501';
  END IF;

  RETURN battles.fn_clone_battle_for_rematch(p_parent_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") IS 'Phase V1: authenticated wrapper. Creates a draft rematch of a terminal parent battle the caller owns. Clones structural fields and rotates contender slots. Returns the new battle id.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_task_prompt" "text" DEFAULT ''::"text", "p_category" "text" DEFAULT NULL::"text", "p_max_contenders" integer DEFAULT 2, "p_is_public" boolean DEFAULT false) RETURNS "battles"."templates"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_row       battles.templates%ROWTYPE;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_title IS NULL OR char_length(trim(p_title)) = 0 THEN
    RAISE EXCEPTION 'title_required' USING ERRCODE = '22023';
  END IF;
  IF p_task_prompt IS NULL OR char_length(trim(p_task_prompt)) = 0 THEN
    RAISE EXCEPTION 'task_prompt_required' USING ERRCODE = '22023';
  END IF;
  IF p_category IS NOT NULL
     AND p_category NOT IN ('creative','technical','business','gaming') THEN
    RAISE EXCEPTION 'invalid_category: %', p_category USING ERRCODE = '22023';
  END IF;
  IF p_max_contenders < 2 THEN
    RAISE EXCEPTION 'max_contenders_too_small' USING ERRCODE = '22023';
  END IF;

  INSERT INTO battles.templates (
    creator_lenser_id, title, description, task_prompt,
    category, max_contenders, is_public
  ) VALUES (
    v_lenser_id, p_title, p_description, p_task_prompt,
    p_category, p_max_contenders, COALESCE(p_is_public, false)
  )
  RETURNING * INTO v_row;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_delete('battle', p_battle_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  UPDATE battles.templates
     SET deleted_at = now(),
         updated_at = now(),
         is_public  = false
   WHERE id                = p_template_id
     AND creator_lenser_id = v_lenser_id
     AND deleted_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_owned' USING ERRCODE = '42501';
  END IF;
END $$;


ALTER FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'xp', 'reputation'
    AS $$
DECLARE
    v_battle RECORD;
    v_winner_id uuid;
    v_top RECORD;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status NOT IN ('voting', 'scoring') THEN
        RAISE EXCEPTION 'Battle must be in voting or scoring status to finalize (current: %)', v_battle.status;
    END IF;

    SELECT contender_id, raw_vote_count
    INTO v_top
    FROM battles.vote_aggregates
    WHERE battle_id = p_battle_id
    ORDER BY raw_vote_count DESC
    LIMIT 1;

    IF v_top IS NOT NULL THEN
        DECLARE
            v_tie_count int;
        BEGIN
            SELECT COUNT(*) INTO v_tie_count
            FROM battles.vote_aggregates
            WHERE battle_id = p_battle_id
              AND raw_vote_count = v_top.raw_vote_count;

            IF v_tie_count > 1 THEN
                v_winner_id := NULL;
            ELSE
                v_winner_id := v_top.contender_id;
            END IF;
        END;
    END IF;

    UPDATE battles.vote_aggregates va
    SET rank_position = sub.rk
    FROM (
        SELECT contender_id,
               ROW_NUMBER() OVER (ORDER BY raw_vote_count DESC, weighted_vote_sum DESC) AS rk
        FROM battles.vote_aggregates
        WHERE battle_id = p_battle_id
    ) sub
    WHERE va.battle_id = p_battle_id
      AND va.contender_id = sub.contender_id;

    UPDATE battles.battles
    SET status = 'closed',
        winner_contender_id = v_winner_id,
        finalized_at = now(),
        updated_at = now()
    WHERE id = p_battle_id;

    -- Phase O3: per-battle ELO update. Best-effort — never roll back finalize.
    BEGIN
      PERFORM public.fn_compute_elo_after_battle(p_battle_id);
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_compute_elo_after_battle failed for battle %: %', p_battle_id, SQLERRM;
    END;
END;
$$;


ALTER FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") RETURNS TABLE("contender_id" "uuid", "vote_value" "text", "is_draw" boolean, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT
    v.voted_contender_id AS contender_id,
    v.vote_value::text   AS vote_value,
    v.is_draw,
    v.updated_at
  FROM battles.votes v
  WHERE v.battle_id       = p_battle_id
    AND v.voter_lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_contenders jsonb;
    v_aggregates jsonb;
    v_result jsonb;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles
    WHERE id = p_battle_id
      AND status IN ('open', 'voting', 'scoring', 'closed', 'published')
      AND deleted_at IS NULL;

    IF v_battle IS NULL THEN
        RETURN NULL;
    END IF;

    -- Build contenders array (no actor_id)
    SELECT COALESCE(jsonb_agg(jsonb_build_object(
        'id', c.id,
        'slot', c.slot,
        'contender_type', c.contender_type::text,
        'display_name', c.display_name,
        'contender_status', c.contender_status
    ) ORDER BY c.slot), '[]'::jsonb)
    INTO v_contenders
    FROM battles.contenders c
    WHERE c.battle_id = p_battle_id;

    -- Build vote aggregates array
    SELECT COALESCE(jsonb_agg(jsonb_build_object(
        'contender_id', va.contender_id,
        'raw_vote_count', va.raw_vote_count,
        'weighted_vote_sum', va.weighted_vote_sum,
        'draw_count', va.draw_count,
        'rank_position', va.rank_position
    ) ORDER BY va.rank_position NULLS LAST), '[]'::jsonb)
    INTO v_aggregates
    FROM battles.vote_aggregates va
    WHERE va.battle_id = p_battle_id;

    -- Compose result
    v_result := jsonb_build_object(
        'id', v_battle.id,
        'title', v_battle.title,
        'slug', v_battle.slug,
        'status', v_battle.status,
        'task_prompt', v_battle.task_prompt,
        'max_contenders', v_battle.max_contenders,
        'invite_code', v_battle.invite_code,
        'total_vote_count', v_battle.total_vote_count,
        'winner_contender_id', v_battle.winner_contender_id,
        'voting_opens_at', v_battle.voting_opens_at,
        'voting_closes_at', v_battle.voting_closes_at,
        'published_at', v_battle.published_at,
        'finalized_at', v_battle.finalized_at,
        'created_at', v_battle.created_at,
        'contenders', v_contenders,
        'vote_aggregates', v_aggregates
    );

    RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "owner_id" "uuid", "webhook_url" "text", "event_types" "text"[], "created_at" timestamp with time zone, "revoked_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_actor UUID;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  RETURN QUERY
    SELECT
      s.id,
      s.battle_id,
      s.owner_id,
      s.webhook_url,
      s.event_types,
      s.created_at,
      s.revoked_at
    FROM battles.battle_event_subscriptions s
   WHERE s.battle_id = p_battle_id
     AND s.owner_id  = v_actor
   ORDER BY s.created_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") IS 'Returns caller-owned webhook subscriptions for a battle. secret_hmac excluded.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") RETURNS "battles"."templates"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_row       battles.templates%ROWTYPE;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  SELECT * INTO v_row
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL
     AND (creator_lenser_id = v_lenser_id OR is_public = true);

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_found' USING ERRCODE = '42501';
  END IF;
  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_battle RECORD;
    v_invited_lenser_id uuid;
    v_invitation_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id != v_lenser_id THEN
        RAISE EXCEPTION 'Only the battle creator can invite';
    END IF;

    IF v_battle.status NOT IN ('draft', 'open') THEN
        RAISE EXCEPTION 'Battle must be in draft or open status to invite (current: %)', v_battle.status;
    END IF;

    -- Try to resolve email to a lenser profile
    SELECT p.id INTO v_invited_lenser_id
    FROM lensers.profiles p
    JOIN auth.users u ON u.id = p.user_id
    WHERE u.email = p_email
    LIMIT 1;

    INSERT INTO battles.invitations (battle_id, invited_by, invited_email, invited_lenser_id)
    VALUES (p_battle_id, v_lenser_id, p_email, v_invited_lenser_id)
    RETURNING id INTO v_invitation_id;

    -- Log the event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        p_battle_id,
        'invitation_sent',
        v_lenser_id,
        jsonb_build_object('invitation_id', v_invitation_id, 'email', p_email)
    );

    RETURN v_invitation_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_lenser RECORD;
    v_battle RECORD;
    v_count int;
    v_next_slot char(1);
    v_contender_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT display_name INTO v_lenser
    FROM lensers.profiles WHERE id = v_lenser_id;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle is not open for joining';
    END IF;

    -- Check if already a contender
    IF EXISTS (
        SELECT 1 FROM battles.contenders
        WHERE battle_id = p_battle_id
          AND contender_ref_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'Already a contender in this battle';
    END IF;

    -- Check contender count
    SELECT COUNT(*) INTO v_count
    FROM battles.contenders WHERE battle_id = p_battle_id;

    IF v_count >= v_battle.max_contenders THEN
        RAISE EXCEPTION 'Battle is full (% / %)', v_count, v_battle.max_contenders;
    END IF;

    -- Assign next slot (A, B, C, ...)
    v_next_slot := chr(65 + v_count);

    INSERT INTO battles.contenders (
        battle_id, slot, contender_type, contender_ref_id,
        display_name, entry_mode, contender_status, joined_at
    )
    VALUES (
        p_battle_id, v_next_slot, 'human', v_lenser_id,
        v_lenser.display_name, 'direct', 'active', now()
    )
    RETURNING id INTO v_contender_id;

    -- Auto-create pending submission
    INSERT INTO battles.submissions (battle_id, contender_id, status)
    VALUES (p_battle_id, v_contender_id, 'pending');

    RETURN v_contender_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid" DEFAULT NULL::"uuid", "p_runner_mode" "text" DEFAULT 'cloud'::"text", "p_device_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id    UUID;
  v_contender_id UUID;
  v_battle       battles.battles%ROWTYPE;
BEGIN
  SELECT p.id INTO v_lenser_id
  FROM   lensers.profiles p
  WHERE  p.user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'battles_join_unauthenticated';
  END IF;

  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'battles_join_not_found: %', p_battle_id;
  END IF;

  IF v_battle.status NOT IN ('open', 'executing') THEN
    RAISE EXCEPTION 'battles_join_not_open: status=%', v_battle.status;
  END IF;

  -- Idempotent: return existing contender if already joined
  SELECT id INTO v_contender_id
  FROM   battles.contenders
  WHERE  battle_id = p_battle_id
  AND    lenser_id = v_lenser_id
  LIMIT  1;

  IF v_contender_id IS NOT NULL THEN
    RETURN v_contender_id;
  END IF;

  -- Check max contenders
  IF (SELECT count(*) FROM battles.contenders WHERE battle_id = p_battle_id) >= v_battle.max_contenders THEN
    RAISE EXCEPTION 'battles_join_full: max_contenders=%', v_battle.max_contenders;
  END IF;

  INSERT INTO battles.contenders (
    battle_id,
    lenser_id,
    contender_status
  )
  VALUES (
    p_battle_id,
    v_lenser_id,
    'active'
  )
  RETURNING id INTO v_contender_id;

  INSERT INTO audit.events (event_type, actor_type, actor_id, payload)
  VALUES (
    'battle.joined',
    'lenser',
    v_lenser_id,
    jsonb_build_object(
      'battle_id',    p_battle_id,
      'agent_id',     p_agent_id,
      'runner_mode',  p_runner_mode,
      'device_id',    p_device_id,
      'workflow_id',  p_workflow_id
    )
  );

  RETURN v_contender_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") RETURNS TABLE("rank" integer, "contender_id" "uuid", "display_name" "text", "score" numeric, "vote_count" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
    RETURN QUERY
    SELECT
        COALESCE(va.rank_position, 0)::integer AS rank,
        c.id AS contender_id,
        c.display_name,
        va.weighted_vote_sum AS score,
        va.raw_vote_count AS vote_count
    FROM battles.contenders c
    LEFT JOIN battles.vote_aggregates va
        ON va.battle_id = c.battle_id AND va.contender_id = c.id
    WHERE c.battle_id = p_battle_id
    ORDER BY va.rank_position NULLS LAST, va.raw_vote_count DESC NULLS LAST;
END;
$$;


ALTER FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  UPDATE battles.battles
  SET forum_thread_id = p_forum_thread_id
  WHERE id = p_battle_id
    AND creator_lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_list_public"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "title" "text", "slug" "text", "status" "text", "contender_count" bigint, "total_vote_count" integer, "creator_display_name" "text", "created_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
BEGIN
    RETURN QUERY
    SELECT
        b.id,
        b.title,
        b.slug,
        b.status::text,
        (SELECT COUNT(*) FROM battles.contenders c WHERE c.battle_id = b.id) AS contender_count,
        b.total_vote_count,
        p.display_name AS creator_display_name,
        b.created_at,
        b.voting_opens_at,
        b.voting_closes_at
    FROM battles.battles b
    JOIN lensers.profiles p ON p.id = b.creator_lenser_id
    WHERE b.status IN ('open', 'voting', 'scoring', 'closed', 'published')
      AND b.deleted_at IS NULL
    ORDER BY b.created_at DESC
    LIMIT p_limit
    OFFSET p_offset;
END;
$$;


ALTER FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_battle           RECORD;
  v_lenser_id        UUID;
  v_was_contender    BOOLEAN := false;
  v_was_voter        BOOLEAN := false;
  v_category         TEXT;
BEGIN
  SELECT b.id, b.status::text AS status, b.template_id, b.slug, t.category
    INTO v_battle
    FROM battles.battles b
    LEFT JOIN battles.templates t ON t.id = b.template_id
   WHERE b.id = p_battle_id
     AND b.deleted_at IS NULL;

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  IF v_battle.status <> 'closed' THEN
    RETURN NULL;
  END IF;

  -- Best-effort resolve caller identity. Anonymous callers leave v_lenser_id
  -- as NULL and fall through to the "create" branch.
  BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
  EXCEPTION WHEN OTHERS THEN
    v_lenser_id := NULL;
  END;

  IF v_lenser_id IS NOT NULL THEN
    SELECT EXISTS (
      SELECT 1 FROM battles.contenders c
       WHERE c.battle_id = p_battle_id
         AND c.contender_ref_id = v_lenser_id
    ) INTO v_was_contender;

    IF NOT v_was_contender THEN
      SELECT EXISTS (
        SELECT 1 FROM battles.votes v
         WHERE v.battle_id = p_battle_id
           AND v.voter_lenser_id = v_lenser_id
      ) INTO v_was_voter;
    END IF;
  END IF;

  v_category := v_battle.category;

  IF v_was_contender THEN
    RETURN jsonb_build_object(
      'action',     'rematch',
      'battle_id',  p_battle_id,
      'slug',       v_battle.slug
    );
  ELSIF v_was_voter THEN
    RETURN jsonb_build_object(
      'action',     'browse',
      'category',   v_category
    );
  ELSE
    RETURN jsonb_build_object(
      'action',      'create',
      'template_id', v_battle.template_id
    );
  END IF;
END $$;


ALTER FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") IS 'Phase BX: contextual next-action recommendation for a closed battle. Returns NULL when battle is not closed or not found. Resolves the caller as contender / voter / other and emits a single CTA shape.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'public'
    AS $$
DECLARE
  rec RECORD;
BEGIN
  FOR rec IN
    SELECT id
      FROM battles.battle_event_subscriptions
     WHERE battle_id  = p_battle_id
       AND revoked_at IS NULL
       AND p_event_type = ANY(event_types)
  LOOP
    INSERT INTO audit.webhook_outbox (subscription_id, battle_id, event_type, payload)
    VALUES (rec.id, p_battle_id, p_event_type, p_payload);
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") IS 'CB: Inserts webhook_outbox entries for matching active subscriptions.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_code text;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles
    WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Only the battle creator can open a battle';
    END IF;

    IF v_battle.status <> 'draft' THEN
        RAISE EXCEPTION 'Battle must be in draft status to open (current: %)', v_battle.status;
    END IF;

    -- Generate invite code if not set
    v_code := v_battle.invite_code;
    IF v_code IS NULL THEN
        v_code := upper(substr(md5(gen_random_uuid()::text), 1, 8));
    END IF;

    UPDATE battles.battles
    SET status = 'open', invite_code = v_code, updated_at = now()
    WHERE id = p_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_lenser_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id != v_lenser_id THEN
        RAISE EXCEPTION 'Only the battle creator can publish';
    END IF;

    IF v_battle.status != 'closed' THEN
        RAISE EXCEPTION 'Battle must be in closed status to publish (current: %)', v_battle.status;
    END IF;

    UPDATE battles.battles
    SET status = 'published',
        published_at = now(),
        updated_at = now()
    WHERE id = p_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_template     RECORD;
  v_rendered     TEXT;
  v_var          RECORD;
  v_value        TEXT;
BEGIN
  SELECT id, task_prompt, creator_lenser_id, is_public
    INTO v_template
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_found' USING ERRCODE = '42501';
  END IF;

  -- Accessibility: owner OR public template OR anonymous read of public.
  IF NOT v_template.is_public
     AND v_template.creator_lenser_id IS DISTINCT FROM auth.uid() THEN
    RAISE EXCEPTION 'template_not_accessible' USING ERRCODE = '42501';
  END IF;

  v_rendered := v_template.task_prompt;

  FOR v_var IN
    SELECT variable_key, default_value, required
      FROM battles.template_prompt_variables
     WHERE template_id = p_template_id
     ORDER BY ordinal, variable_key
  LOOP
    v_value := COALESCE(
      NULLIF(p_variables->>v_var.variable_key, ''),
      v_var.default_value
    );

    IF v_value IS NULL THEN
      IF v_var.required THEN
        RAISE EXCEPTION 'missing_required_variable: %', v_var.variable_key
          USING ERRCODE = '22023';
      END IF;
      v_value := '';
    END IF;

    -- Plain text substitution, including the braces.
    v_rendered := REPLACE(
      v_rendered,
      '{{' || v_var.variable_key || '}}',
      v_value
    );
  END LOOP;

  RETURN v_rendered;
END $$;


ALTER FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('battle', p_battle_id); $$;


ALTER FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_archive('battle', p_battle_id);
END;
$$;


ALTER FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_actor UUID;
  v_rows  INT;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  UPDATE battles.battle_event_subscriptions
     SET revoked_at = now()
   WHERE id       = p_subscription_id
     AND owner_id = v_actor
     AND revoked_at IS NULL;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  IF v_rows = 0 THEN
    RAISE EXCEPTION 'Subscription not found or already revoked'
      USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") IS 'Soft-revokes a caller-owned webhook subscription. Idempotent-safe (raises P0002 if already revoked).';



CREATE OR REPLACE FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'pg_catalog', 'public', 'battles', 'lensers'
    AS $_$
DECLARE
  v_caller    uuid;
  v_owner     uuid;
  v_series_id uuid;
  v_slug      text;
  v_parts     text[];
BEGIN
  v_caller := lensers.get_auth_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
  END IF;

  IF p_name IS NULL OR length(btrim(p_name)) = 0 THEN
    RAISE EXCEPTION 'series_name_required' USING ERRCODE = '22023';
  END IF;

  v_parts := regexp_split_to_array(btrim(COALESCE(p_cron_expr, '')), '\s+');
  IF array_length(v_parts, 1) <> 5 THEN
    RAISE EXCEPTION 'invalid_cron_expression: expected 5 fields'
      USING ERRCODE = '22023';
  END IF;

  SELECT creator_lenser_id INTO v_owner
    FROM battles.battles
   WHERE id = p_seed_battle_id
     AND deleted_at IS NULL;

  IF v_owner IS NULL THEN
    RAISE EXCEPTION 'seed_battle_not_found: %', p_seed_battle_id
      USING ERRCODE = 'P0002';
  END IF;

  IF v_owner IS DISTINCT FROM v_caller THEN
    RAISE EXCEPTION 'not_battle_owner' USING ERRCODE = '42501';
  END IF;

  -- Slug: kebab-case from name + 6 random hex; bounded to <= 128 chars.
  v_slug := substr(
    regexp_replace(
      regexp_replace(lower(p_name), '[^a-z0-9]+', '-', 'g'),
      '(^-+|-+$)', '', 'g'
    ),
    1, 110
  );
  IF length(v_slug) = 0 THEN
    v_slug := 'series';
  END IF;
  v_slug := v_slug || '-' || substr(replace(gen_random_uuid()::text, '-', ''), 1, 6);

  INSERT INTO battles.series (
    creator_lenser_id, name, slug, seed_battle_id, cron_expr,
    next_dispatch_at, is_active
  )
  VALUES (
    v_caller, btrim(p_name), v_slug, p_seed_battle_id, btrim(p_cron_expr),
    -- Earliest the dispatcher can pick it up: the next top-of-hour tick.
    date_trunc('hour', now()) + interval '1 hour',
    true
  )
  RETURNING id INTO v_series_id;

  -- Seed at position=1.
  INSERT INTO battles.series_battles (series_id, battle_id, position)
  VALUES (v_series_id, p_seed_battle_id, 1);

  RETURN v_series_id;
END;
$_$;


ALTER FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") IS 'Phase V4: create a battle series anchored to a seed battle the caller owns. Inserts the seed at position=1 and primes next_dispatch_at to the next top-of-hour tick. Returns the new series id.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_battle RECORD;
    v_pending int;
BEGIN
    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.creator_lenser_id <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Only the battle creator can start voting';
    END IF;

    IF v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle must be open to start voting (current: %)', v_battle.status;
    END IF;

    -- Check all contenders have submitted
    SELECT COUNT(*) INTO v_pending
    FROM battles.submissions s
    WHERE s.battle_id = p_battle_id AND s.status = 'pending';

    IF v_pending > 0 THEN
        RAISE EXCEPTION '% contender(s) have not submitted yet', v_pending;
    END IF;

    UPDATE battles.battles
    SET status = 'voting',
        voting_opens_at = now(),
        voting_closes_at = p_voting_closes_at,
        updated_at = now()
    WHERE id = p_battle_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text" DEFAULT NULL::"text", "p_content_url" "text" DEFAULT NULL::"text", "p_content_media" "jsonb" DEFAULT NULL::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_contender RECORD;
    v_battle RECORD;
    v_submission_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL OR v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle is not open for submissions';
    END IF;

    SELECT * INTO v_contender
    FROM battles.contenders
    WHERE battle_id = p_battle_id
      AND contender_type = 'human'
      AND contender_ref_id = v_lenser_id;

    IF v_contender IS NULL THEN
        RAISE EXCEPTION 'You are not a contender in this battle';
    END IF;

    IF p_content_text IS NULL AND p_content_url IS NULL THEN
        RAISE EXCEPTION 'At least content_text or content_url is required';
    END IF;

    UPDATE battles.submissions
    SET status = 'submitted',
        content_text = p_content_text,
        content_url = p_content_url,
        content_media = COALESCE(p_content_media, '[]'::jsonb),
        submitted_at = now(),
        updated_at = now()
    WHERE battle_id = p_battle_id AND contender_id = v_contender.id
    RETURNING id INTO v_submission_id;

    RETURN v_submission_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text" DEFAULT NULL::"text", "p_content_url" "text" DEFAULT NULL::"text", "p_content_media" "jsonb" DEFAULT NULL::"jsonb", "p_execution_run_id" "uuid" DEFAULT NULL::"uuid", "p_artifact_id" "uuid" DEFAULT NULL::"uuid", "p_source_type" "text" DEFAULT 'manual'::"text", "p_adapter_id" "uuid" DEFAULT NULL::"uuid", "p_model_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'execution'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_contender RECORD;
    v_battle RECORD;
    v_submission_id uuid;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL OR v_battle.status <> 'open' THEN
        RAISE EXCEPTION 'Battle is not open for submissions';
    END IF;

    -- Look up contender by contender_ref_id only
    SELECT * INTO v_contender
    FROM battles.contenders
    WHERE battle_id = p_battle_id
      AND contender_ref_id = v_lenser_id;

    IF v_contender IS NULL THEN
        RAISE EXCEPTION 'You are not a contender in this battle';
    END IF;

    IF p_content_text IS NULL AND p_content_url IS NULL AND p_execution_run_id IS NULL THEN
        RAISE EXCEPTION 'At least content_text, content_url, or execution_run_id is required';
    END IF;

    UPDATE battles.submissions
    SET status = 'submitted',
        content_text = p_content_text,
        content_url = p_content_url,
        content_media = COALESCE(p_content_media, '[]'::jsonb),
        submitted_at = now(),
        updated_at = now(),
        execution_run_id = p_execution_run_id,
        artifact_id = p_artifact_id,
        source_type = p_source_type,
        adapter_id = p_adapter_id,
        model_id = p_model_id
    WHERE battle_id = p_battle_id AND contender_id = v_contender.id
    RETURNING id INTO v_submission_id;

    -- Link execution to submission if provided
    IF p_execution_run_id IS NOT NULL THEN
        INSERT INTO execution.links (run_id, entity_type, entity_id)
        VALUES (p_execution_run_id, 'submission', v_submission_id)
        ON CONFLICT DO NOTHING;
    END IF;

    RETURN v_submission_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") RETURNS "battles"."submissions"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid       UUID := auth.uid();
  v_row       battles.submissions%ROWTYPE;
  v_is_owner  BOOLEAN;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_output_modality IS NULL
     OR p_output_modality NOT IN ('image', 'video', 'audio') THEN
    RAISE EXCEPTION 'invalid_output_modality: %', p_output_modality
      USING ERRCODE = '22023';
  END IF;
  IF p_media_url IS NULL OR char_length(p_media_url) = 0 THEN
    RAISE EXCEPTION 'media_url_required' USING ERRCODE = '22023';
  END IF;

  SELECT EXISTS (
    SELECT 1
      FROM battles.contenders c
      JOIN battles.contender_entity_map cem ON cem.contender_id = c.id
     WHERE c.id           = p_contender_id
       AND c.battle_id    = p_battle_id
       AND cem.profile_id = v_uid
  )
  INTO v_is_owner;

  IF NOT v_is_owner THEN
    RAISE EXCEPTION 'contender_not_owned' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.submissions (
    battle_id,
    contender_id,
    content_url,
    media_url,
    mime_type,
    output_modality,
    status
  ) VALUES (
    p_battle_id,
    p_contender_id,
    p_media_url,
    p_media_url,
    p_mime_type,
    p_output_modality,
    'submitted'
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE
    SET content_url     = EXCLUDED.content_url,
        media_url       = EXCLUDED.media_url,
        mime_type       = EXCLUDED.mime_type,
        output_modality = EXCLUDED.output_modality,
        status          = 'submitted',
        submitted_at    = COALESCE(battles.submissions.submitted_at, now())
  RETURNING * INTO v_row;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_actor UUID;
  v_sub_id UUID;
  v_secret TEXT;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  -- Generate HMAC-style secret (random 32 bytes hex-encoded)
  v_secret := encode(gen_random_bytes(32), 'hex');

  INSERT INTO battles.battle_event_subscriptions
    (battle_id, owner_id, webhook_url, event_types, secret_hmac)
  VALUES
    (p_battle_id, v_actor, p_webhook_url, p_event_types, v_secret)
  RETURNING id INTO v_sub_id;

  RETURN v_sub_id;
END;
$$;


ALTER FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) IS 'CB: Creates a webhook subscription with generated HMAC secret. Owner-only.';



CREATE OR REPLACE FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text" DEFAULT NULL::"text", "p_description" "text" DEFAULT NULL::"text", "p_task_prompt" "text" DEFAULT NULL::"text", "p_category" "text" DEFAULT NULL::"text", "p_max_contenders" integer DEFAULT NULL::integer, "p_is_public" boolean DEFAULT NULL::boolean) RETURNS "battles"."templates"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_row       battles.templates%ROWTYPE;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_category IS NOT NULL
     AND p_category NOT IN ('creative','technical','business','gaming') THEN
    RAISE EXCEPTION 'invalid_category: %', p_category USING ERRCODE = '22023';
  END IF;
  IF p_max_contenders IS NOT NULL AND p_max_contenders < 2 THEN
    RAISE EXCEPTION 'max_contenders_too_small' USING ERRCODE = '22023';
  END IF;

  UPDATE battles.templates t
     SET title          = COALESCE(p_title,          t.title),
         description    = COALESCE(p_description,    t.description),
         task_prompt    = COALESCE(p_task_prompt,    t.task_prompt),
         category       = COALESCE(p_category,       t.category),
         max_contenders = COALESCE(p_max_contenders, t.max_contenders),
         is_public      = COALESCE(p_is_public,      t.is_public),
         updated_at     = now()
   WHERE t.id                = p_template_id
     AND t.creator_lenser_id = v_lenser_id
     AND t.deleted_at IS NULL
   RETURNING * INTO v_row;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'template_not_owned' USING ERRCODE = '42501';
  END IF;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_battle RECORD;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status <> 'voting' THEN
        RAISE EXCEPTION 'Battle is not in voting phase';
    END IF;

    -- Check voting window
    IF v_battle.voting_closes_at IS NOT NULL AND now() > v_battle.voting_closes_at THEN
        RAISE EXCEPTION 'Voting window has closed';
    END IF;

    -- Prevent contenders from voting on their own battle
    IF EXISTS (
        SELECT 1 FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
          AND c.contender_type = 'human'
          AND c.contender_ref_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'Contenders cannot vote on their own battle';
    END IF;

    -- Delete existing vote if any, then insert new
    DELETE FROM battles.votes
    WHERE battle_id = p_battle_id AND voter_lenser_id = v_lenser_id;

    INSERT INTO battles.votes (battle_id, voter_lenser_id, vote_value, rationale)
    VALUES (p_battle_id, v_lenser_id, p_vote, p_rationale);
END;
$$;


ALTER FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text" DEFAULT NULL::"text", "p_contender_id" "uuid" DEFAULT NULL::"uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
    v_lenser_id uuid;
    v_battle RECORD;
    v_voted_contender_id uuid;
    v_is_draw boolean := false;
BEGIN
    v_lenser_id := lensers.get_auth_lenser_id();
    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT * INTO v_battle
    FROM battles.battles WHERE id = p_battle_id;

    IF v_battle IS NULL THEN
        RAISE EXCEPTION 'Battle not found';
    END IF;

    IF v_battle.status <> 'voting' THEN
        RAISE EXCEPTION 'Battle is not in voting phase';
    END IF;

    -- Check voting window
    IF v_battle.voting_closes_at IS NOT NULL AND now() > v_battle.voting_closes_at THEN
        RAISE EXCEPTION 'Voting window has closed';
    END IF;

    -- Prevent contenders from voting on their own battle
    IF EXISTS (
        SELECT 1 FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
          AND c.contender_ref_id = v_lenser_id
    ) THEN
        RAISE EXCEPTION 'Contenders cannot vote on their own battle';
    END IF;

    -- Resolve voted_contender_id
    IF p_contender_id IS NOT NULL THEN
        -- Direct contender vote
        IF NOT EXISTS (
            SELECT 1 FROM battles.contenders WHERE id = p_contender_id AND battle_id = p_battle_id
        ) THEN
            RAISE EXCEPTION 'Contender not found in this battle';
        END IF;
        v_voted_contender_id := p_contender_id;
        v_is_draw := false;
    ELSIF p_vote = 'draw' THEN
        v_voted_contender_id := NULL;
        v_is_draw := true;
    ELSE
        -- Legacy slot-based vote: resolve from vote_value
        SELECT c.id INTO v_voted_contender_id
        FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
          AND c.slot = CASE p_vote
              WHEN 'contender_a' THEN 'A'::bpchar
              WHEN 'contender_b' THEN 'B'::bpchar
          END;
        v_is_draw := false;
    END IF;

    -- Insert the vote (no voter_actor_id)
    INSERT INTO battles.votes (
        battle_id, voter_lenser_id, vote_value, rationale,
        voted_contender_id, is_draw, weight, is_ai_vote
    )
    VALUES (
        p_battle_id, v_lenser_id, p_vote, p_rationale,
        v_voted_contender_id, v_is_draw, 1.0, false
    );

    -- Update vote_aggregates
    IF v_voted_contender_id IS NOT NULL AND NOT v_is_draw THEN
        INSERT INTO battles.vote_aggregates (battle_id, contender_id, raw_vote_count, weighted_vote_sum)
        VALUES (p_battle_id, v_voted_contender_id, 1, 1.0)
        ON CONFLICT (battle_id, contender_id) DO UPDATE
        SET raw_vote_count = battles.vote_aggregates.raw_vote_count + 1,
            weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1.0,
            updated_at = now();
    ELSIF v_is_draw THEN
        INSERT INTO battles.vote_aggregates (battle_id, contender_id, draw_count)
        SELECT p_battle_id, c.id, 1
        FROM battles.contenders c
        WHERE c.battle_id = p_battle_id
        ON CONFLICT (battle_id, contender_id) DO UPDATE
        SET draw_count = battles.vote_aggregates.draw_count + 1,
            updated_at = now();
    END IF;

    -- Log the vote event
    INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
    VALUES (
        p_battle_id,
        'vote_cast',
        v_lenser_id,
        jsonb_build_object(
            'vote_value', p_vote::text,
            'contender_id', v_voted_contender_id,
            'is_draw', v_is_draw
        )
    );
END;
$$;


ALTER FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id uuid;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  IF v_source_id = p_target_profile_id THEN
    RAISE EXCEPTION 'Cannot block yourself';
  END IF;

  -- Upsert block
  INSERT INTO lensers.relationships (source_profile_id, target_profile_id, status)
  VALUES (v_source_id, p_target_profile_id, 'blocked')
  ON CONFLICT (source_profile_id, target_profile_id)
  DO UPDATE SET status = 'blocked', removed_at = now();

  -- Remove their follow of us if it exists
  UPDATE lensers.relationships
  SET status = 'removed', removed_at = now()
  WHERE source_profile_id = p_target_profile_id
    AND target_profile_id = v_source_id
    AND status = 'accepted';

  RETURN jsonb_build_object('blocked', true);
END;
$$;


ALTER FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_browse_battles"("p_category" "text" DEFAULT NULL::"text", "p_status" "text" DEFAULT NULL::"text", "p_q" "text" DEFAULT NULL::"text", "p_after_created" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_after_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "title" "text", "slug" "text", "status" "text", "category" "text", "contender_count" integer, "vote_count" integer, "created_at" timestamp with time zone, "template_title" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
  WITH bounded AS (
    SELECT
      b.id,
      b.title,
      b.slug,
      b.status::text,
      t.category,
      b.total_vote_count,
      b.created_at,
      t.title AS template_title
    FROM battles.battles b
    LEFT JOIN battles.templates t ON t.id = b.template_id
    WHERE b.deleted_at IS NULL
      AND (
        -- Anonymous browse only exposes published / voting battles. Owners
        -- can see all their own via the existing repository methods.
        b.status::text IN ('open', 'voting', 'scoring', 'published')
      )
      AND (p_category IS NULL OR t.category = p_category)
      AND (p_status   IS NULL OR b.status::text = p_status)
      AND (
        p_q IS NULL
        OR to_tsvector('simple', b.title) @@ plainto_tsquery('simple', p_q)
      )
      AND (
        p_after_created IS NULL
        OR (b.created_at, b.id) < (p_after_created, COALESCE(p_after_id, '00000000-0000-0000-0000-000000000000'::uuid))
      )
    ORDER BY b.created_at DESC, b.id DESC
    LIMIT LEAST(GREATEST(p_limit, 1), 100)
  )
  SELECT
    bounded.id,
    bounded.title,
    bounded.slug,
    bounded.status,
    bounded.category,
    COALESCE((SELECT count(*)::int FROM battles.contenders c WHERE c.battle_id = bounded.id), 0)
      AS contender_count,
    COALESCE(bounded.total_vote_count, 0) AS vote_count,
    bounded.created_at,
    bounded.template_title
  FROM bounded
  ORDER BY bounded.created_at DESC, bounded.id DESC;
$$;


ALTER FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") RETURNS TABLE("provider" "text", "key_hint" "text", "label" "text", "is_valid" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'execution', 'agents', 'lensers', 'public'
    AS $$
  SELECT
    bk.provider,
    bk.key_hint,
    bk.label,
    (bk.revoked_at IS NULL AND (bk.expires_at IS NULL OR bk.expires_at > now())) AS is_valid
  FROM execution.byok_keys bk
  WHERE bk.agent_id = p_agent_id
    AND EXISTS (
      SELECT 1 FROM agents.ownerships o
      WHERE o.ai_lenser_id    = p_agent_id
        AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
        AND o.revoked_at IS NULL
    );
$$;


ALTER FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text" DEFAULT NULL::"text", "p_label" "text" DEFAULT NULL::"text", "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_key_id uuid;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships o
    WHERE  o.ai_lenser_id   = p_agent_id
      AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND  o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'byok_key_register_forbidden: agent % not owned by caller', p_agent_id;
  END IF;

  INSERT INTO execution.byok_keys (agent_id, provider, key_encrypted, key_hint, label, expires_at)
  VALUES (p_agent_id, p_provider, p_key_encrypted, p_key_hint, p_label, p_expires_at)
  ON CONFLICT (agent_id, provider) DO UPDATE
    SET key_encrypted = EXCLUDED.key_encrypted,
        key_hint      = EXCLUDED.key_hint,
        label         = EXCLUDED.label,
        expires_at    = EXCLUDED.expires_at,
        revoked_at    = NULL,
        created_at    = now()
  RETURNING id INTO v_key_id;

  RETURN v_key_id;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text" DEFAULT NULL::"text") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row     execution.byok_keys%ROWTYPE;
  v_require boolean;
BEGIN
  SELECT byok_require_reservation INTO v_require FROM billing.runtime_settings WHERE id = 1;
  IF COALESCE(v_require, false) THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_MISSING: legacy fn_byok_key_resolve is disabled; use fn_byok_key_resolve_v2'
      USING ERRCODE = 'P0001';
  END IF;

  SELECT * INTO v_row
  FROM   execution.byok_keys
  WHERE  agent_id   = p_agent_id
    AND  provider   = p_provider
    AND  revoked_at IS NULL
    AND  (expires_at IS NULL OR expires_at > now());

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  IF p_model_id IS NOT NULL
     AND v_row.allowed_model_ids IS NOT NULL
     AND NOT (p_model_id = ANY (v_row.allowed_model_ids)) THEN
    RAISE EXCEPTION 'byok_key_resolve: model % is not in allowed_model_ids for provider %', p_model_id, p_provider
      USING ERRCODE = 'P0001';
  END IF;

  RETURN v_row.key_encrypted;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") IS 'AR / CG1: legacy BYOK resolve. Preserved for backward compatibility with lf-gatewayd <= 0.10. Raises E_BYOK_CONTEXT_MISSING when billing.runtime_settings.byok_require_reservation = true.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'billing', 'agents', 'ai', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_key   execution.byok_keys%ROWTYPE;
  v_res   billing.cost_reservations%ROWTYPE;
  v_model_key text;
BEGIN
  IF p_reservation_id IS NULL THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_MISSING: reservation_id is required'
      USING ERRCODE = 'P0001';
  END IF;

  SELECT * INTO v_res FROM billing.cost_reservations WHERE id = p_reservation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation % not found', p_reservation_id
      USING ERRCODE = 'P0001';
  END IF;

  IF v_res.status <> 'held' THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation % is not held (status=%)',
      p_reservation_id, v_res.status
      USING ERRCODE = 'P0001';
  END IF;
  IF v_res.held_until <= now() THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_EXPIRED: reservation % held_until passed', p_reservation_id
      USING ERRCODE = 'P0001';
  END IF;
  IF v_res.ai_lenser_id <> p_agent_id THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation/agent mismatch'
      USING ERRCODE = 'P0001';
  END IF;
  IF v_res.provider_key <> p_provider THEN
    RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation/provider mismatch'
      USING ERRCODE = 'P0001';
  END IF;

  -- Optional model binding: if p_model_id is given, it must match the
  -- reservation's model_id (look up via ai.models.key).
  IF p_model_id IS NOT NULL AND LENGTH(p_model_id) > 0 THEN
    SELECT key INTO v_model_key FROM ai.models WHERE id = v_res.model_id;
    IF v_model_key IS DISTINCT FROM p_model_id THEN
      RAISE EXCEPTION 'E_BYOK_CONTEXT_INVALID: reservation/model mismatch (expected %, got %)',
        v_model_key, p_model_id
        USING ERRCODE = 'P0001';
    END IF;
  END IF;

  SELECT * INTO v_key
  FROM   execution.byok_keys
  WHERE  agent_id   = p_agent_id
    AND  provider   = p_provider
    AND  revoked_at IS NULL
    AND  (expires_at IS NULL OR expires_at > now());
  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  IF p_model_id IS NOT NULL
     AND v_key.allowed_model_ids IS NOT NULL
     AND NOT (p_model_id = ANY (v_key.allowed_model_ids)) THEN
    RAISE EXCEPTION 'byok_key_resolve_v2: model % is not in allowed_model_ids for provider %',
      p_model_id, p_provider
      USING ERRCODE = 'P0001';
  END IF;

  -- Audit row: prefer caller-supplied model id, else fall back to the model
  -- key bound by the reservation. fn_byok_log_usage demands NOT NULL.
  IF p_model_id IS NOT NULL AND LENGTH(p_model_id) > 0 THEN
    PERFORM public.fn_byok_log_usage(v_key.id, NULL, p_model_id, 0);
  ELSIF v_model_key IS NOT NULL THEN
    PERFORM public.fn_byok_log_usage(v_key.id, NULL, v_model_key, 0);
  ELSE
    SELECT m.key INTO v_model_key FROM ai.models m WHERE m.id = v_res.model_id;
    PERFORM public.fn_byok_log_usage(v_key.id, NULL, COALESCE(v_model_key, 'unknown'), 0);
  END IF;

  RETURN v_key.key_encrypted;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") IS 'CG1: BYOK key resolution bound to a held reservation. The reservation_id is the per-call unforgeable token. Service_role only. Logs every resolve to audit.byok_key_usage via fn_byok_log_usage.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'agents', 'lensers', 'public'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships o
    WHERE  o.ai_lenser_id   = p_agent_id
      AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND  o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'byok_key_revoke_forbidden';
  END IF;

  UPDATE execution.byok_keys
  SET    revoked_at = now()
  WHERE  agent_id = p_agent_id
    AND  provider  = p_provider;
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'audit', 'public'
    AS $$
DECLARE
  v_actor_id UUID;
  v_rows_updated INT;
BEGIN
  v_actor_id := auth.uid();
  IF v_actor_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    JOIN lensers.profiles p ON p.id = al.profile_id
    WHERE al.id = p_agent_id AND p.id = v_actor_id
  ) THEN
    RAISE EXCEPTION 'byok_key_rotate: caller does not own agent %', p_agent_id
      USING ERRCODE = 'P0001';
  END IF;

  UPDATE execution.byok_keys
  SET
    key_encrypted  = p_new_encrypted,
    key_hint       = p_new_hint,
    revoked_at     = NULL,
    created_at     = now(),
    last_rotated_at = now()
  WHERE agent_id = p_agent_id
    AND provider  = p_provider;

  GET DIAGNOSTICS v_rows_updated = ROW_COUNT;
  IF v_rows_updated = 0 THEN
    RAISE EXCEPTION 'byok_key_rotate: no key found for agent % provider %', p_agent_id, p_provider
      USING ERRCODE = 'P0001';
  END IF;

  INSERT INTO audit.events (event_type, actor_type, severity, payload)
  VALUES (
    'byok.key_rotated',
    'user',
    'info',
    jsonb_build_object(
      'agent_id',      p_agent_id,
      'provider',      p_provider,
      'key_hint',      p_new_hint,
      'rotated_by',    v_actor_id,
      'rotated_at',    now()
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") IS 'AR: Rotates an existing BYOK key. Requires the caller to be the agent owner. Atomic UPDATE + audit.events INSERT. SECURITY DEFINER; authenticated role allowed.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
BEGIN
  INSERT INTO audit.byok_key_usage (key_id, battle_id, model_id, token_count, caller_role)
  VALUES (p_key_id, p_battle_id, p_model_id, COALESCE(p_token_count, 0), 'service_role');
END;
$$;


ALTER FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) IS 'BZ: Inserts a usage record into audit.byok_key_usage. Called fire-and-forget after each successful model call. SECURITY DEFINER; service_role only. Never exposes key material.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_rotation_due"() RETURNS SETOF "execution"."byok_keys"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
  SELECT *
  FROM execution.byok_keys
  WHERE revoked_at IS NULL
    AND last_rotated_at < now() - INTERVAL '90 days'
  ORDER BY last_rotated_at ASC;
$$;


ALTER FUNCTION "public"."fn_byok_rotation_due"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_rotation_due"() IS 'BZ: Returns byok_keys rows where last_rotated_at < now() - 90 days and not revoked. Used by the daily rotation-due cron and lf byok check-rotation CLI command.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_send_rotation_notifications"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'audit', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_key   execution.byok_keys%ROWTYPE;
  v_owner UUID;
  v_count INT := 0;
BEGIN
  FOR v_key IN (
    SELECT * FROM execution.byok_keys
    WHERE revoked_at    IS NULL
      AND last_rotated_at < now() - INTERVAL '90 days'
  ) LOOP
    -- Resolve owner: byok_keys → ai_lensers → profiles
    SELECT lp.id INTO v_owner
    FROM agents.ai_lensers al
    JOIN lensers.profiles lp ON lp.id = al.profile_id
    WHERE al.id = v_key.agent_id
    LIMIT 1;

    IF v_owner IS NOT NULL THEN
      PERFORM public.fn_send_notification(
        v_owner,
        'byok_rotation_due',
        jsonb_build_object(
          'key_id',          v_key.id,
          'provider',        v_key.provider,
          'agent_id',        v_key.agent_id,
          'last_rotated_at', v_key.last_rotated_at,
          'days_overdue',    EXTRACT(DAY FROM now() - v_key.last_rotated_at - INTERVAL '90 days')::int
        )
      );
      v_count := v_count + 1;
    END IF;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_byok_send_rotation_notifications"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_send_rotation_notifications"() IS 'BZ: Sends byok_rotation_due notifications for all overdue keys. Called by daily pg_cron job. Returns count of notifications sent.';



CREATE OR REPLACE FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_actor_id       UUID;
  v_ai_lenser_id   UUID;
  v_key_row        execution.byok_keys%ROWTYPE;
  v_allowed_models TEXT[];
BEGIN
  -- Must be authenticated
  v_actor_id := auth.uid();
  IF v_actor_id IS NULL THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'authentication_required');
  END IF;

  -- Resolve the ai_lenser_id for this contender
  SELECT cem.ai_lenser_id INTO v_ai_lenser_id
  FROM battles.contender_entity_map cem
  WHERE cem.contender_id = p_contender_id
    AND cem.ai_lenser_id IS NOT NULL
  LIMIT 1;

  IF v_ai_lenser_id IS NULL THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'contender_has_no_agent');
  END IF;

  -- Caller must own this agent
  IF NOT EXISTS (
    SELECT 1
    FROM agents.ai_lensers al
    JOIN lensers.profiles lp ON lp.id = al.profile_id
    WHERE al.id = v_ai_lenser_id
      AND lp.id = v_actor_id
  ) THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'caller_does_not_own_agent');
  END IF;

  -- Get the first valid key for this agent (provider-agnostic validation)
  SELECT * INTO v_key_row
  FROM execution.byok_keys bk
  WHERE bk.agent_id  = v_ai_lenser_id
    AND bk.revoked_at IS NULL
    AND (bk.expires_at IS NULL OR bk.expires_at > now())
  ORDER BY bk.created_at DESC
  LIMIT 1;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('valid', false, 'reason', 'no_valid_key');
  END IF;

  -- Check model allowlist against battle template
  SELECT bt.allowed_model_ids INTO v_allowed_models
  FROM battles.battles b
  JOIN battles.templates bt ON bt.id = b.template_id
  WHERE b.id = p_battle_id;

  IF v_allowed_models IS NOT NULL
    AND array_length(v_allowed_models, 1) > 0
    AND v_key_row.allowed_model_ids IS NOT NULL
  THEN
    -- Verify intersection is non-empty
    IF NOT EXISTS (
      SELECT 1
      FROM unnest(v_key_row.allowed_model_ids) AS km
      WHERE km = ANY(v_allowed_models)
    ) THEN
      RETURN jsonb_build_object(
        'valid', false,
        'reason', 'model_not_in_template_allowlist'
      );
    END IF;
  END IF;

  RETURN jsonb_build_object('valid', true, 'reason', null, 'key_id', v_key_row.id);
END;
$$;


ALTER FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") IS 'BZ: Validates that the contender in a battle has a usable BYOK key. Returns {valid, reason} JSON. SECURITY DEFINER; authenticated owner only.';



CREATE OR REPLACE FUNCTION "public"."fn_cancel_account_deletion_on_login"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_profile RECORD;
  v_event_type text;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RETURN jsonb_build_object('restored', false, 'reason', 'not_authenticated');
  END IF;

  SELECT id, user_id, status, deletion_deadline_at, deletion_requested_at
  INTO v_profile
  FROM lensers.profiles
  WHERE user_id = v_uid
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('restored', false, 'reason', 'no_profile');
  END IF;

  IF v_profile.status = 'pending_deletion'
     AND (v_profile.deletion_deadline_at IS NULL OR v_profile.deletion_deadline_at > now()) THEN
    UPDATE lensers.profiles
    SET status = 'active',
        deletion_requested_at = null,
        deletion_deadline_at = null,
        updated_at = now()
    WHERE id = v_profile.id;

    PERFORM lensers.log_account_lifecycle_event(
      v_profile.id,
      v_profile.user_id,
      'restored_from_pending_deletion',
      v_profile.status,
      'active'::lensers.lenser_status,
      'signin_restore',
      jsonb_build_object(
        'restored_before_deadline', true,
        'deletion_deadline_at', v_profile.deletion_deadline_at
      )
    );

    RETURN jsonb_build_object('restored', true, 'from_status', 'pending_deletion');
  END IF;

  IF (
    v_profile.status = 'deactivated' OR
    (v_profile.status = 'active' AND v_profile.deletion_requested_at IS NOT NULL)
  ) AND (
    v_profile.deletion_deadline_at IS NULL OR v_profile.deletion_deadline_at > now()
  ) THEN
    UPDATE lensers.profiles
    SET status = 'active',
        deletion_requested_at = null,
        deletion_deadline_at = null,
        updated_at = now()
    WHERE id = v_profile.id;

    v_event_type := CASE
      WHEN v_profile.status = 'active' THEN 'restored_from_pending_deletion'
      ELSE 'restored_from_deactivated'
    END;

    PERFORM lensers.log_account_lifecycle_event(
      v_profile.id,
      v_profile.user_id,
      v_event_type,
      v_profile.status,
      'active'::lensers.lenser_status,
      'signin_restore',
      jsonb_build_object(
        'restored_before_deadline', true,
        'deletion_deadline_at', v_profile.deletion_deadline_at,
        'cleared_stale_deletion_flags', v_profile.status = 'active'
      )
    );

    RETURN jsonb_build_object('restored', true, 'from_status', v_profile.status);
  END IF;

  RETURN jsonb_build_object('restored', false, 'reason', 'no_action_needed');
END;
$$;


ALTER FUNCTION "public"."fn_cancel_account_deletion_on_login"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
BEGIN
  UPDATE agents.team_runs
  SET status = 'cancelled'
  WHERE id = p_team_run_id
    AND ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = v_lenser_id
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    );
END;
$$;


ALTER FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") RETURNS TABLE("cancelled_run_count" integer, "cancelled_job_count" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'battles', 'public', 'lensers'
    AS $$
DECLARE
  v_profile_id UUID;
  v_run_count  INT;
  v_job_count  INT;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'cancel_all_active_runs_forbidden: admin role required';
  END IF;

  SELECT id INTO v_profile_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  -- 1. Cancel all active / queued workflow runs and cascade to node results in
  --    one atomic CTE. The RETURNING ids are used directly in the cascade UPDATE
  --    to avoid any time-window ambiguity.
  WITH cancelled_runs AS (
    UPDATE lenses.workflow_runs
    SET    status       = 'cancelled',
           completed_at = COALESCE(completed_at, now())
    WHERE  status IN ('running', 'streaming', 'recovered', 'queued', 'pending')
    RETURNING id
  ),
  node_cascade AS (
    UPDATE lenses.workflow_node_results nr
    SET    status        = 'cancelled',
           completed_at  = COALESCE(nr.completed_at, now()),
           error_message = COALESCE(nr.error_message, p_reason)
    FROM   cancelled_runs cr
    WHERE  nr.run_id = cr.id
      AND  nr.status NOT IN (
             'completed', 'failed', 'cancelled',
             'timed_out', 'skipped', 'blocked', 'invalidated'
           )
    RETURNING 1
  )
  SELECT COUNT(DISTINCT cr.id)::INT INTO v_run_count
  FROM   cancelled_runs cr;

  -- 2. Cancel all active / queued battle execution jobs.
  WITH cancelled_jobs AS (
    UPDATE battles.battle_execution_jobs
    SET    status       = 'failed',
           completed_at = now(),
           error_message = COALESCE(error_message, p_reason)
    WHERE  status IN ('queued', 'claimed', 'running')
    RETURNING id
  )
  SELECT COUNT(*)::INT INTO v_job_count FROM cancelled_jobs;

  -- 3. Audit trail.
  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'emergency.cancel_all',
    'lenser',
    v_profile_id,
    'warn',
    jsonb_build_object(
      'reason',          p_reason,
      'cancelled_runs',  v_run_count,
      'cancelled_jobs',  v_job_count
    )
  );

  RETURN QUERY SELECT v_run_count, v_job_count;
END;
$$;


ALTER FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") IS 'Admin-only: atomically cancel ALL active workflow runs (and their node results) plus all active battle execution jobs. Returns (cancelled_run_count, cancelled_job_count). Uses a CTE to keep run and node-result cancellation in one transaction.';



CREATE OR REPLACE FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run agents.team_runs;
BEGIN
  SELECT * INTO v_run
    FROM agents.team_runs
   WHERE id = p_team_run_id
   FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'team run not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_run.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF v_run.status IN ('completed','failed','cancelled') THEN
    RAISE EXCEPTION 'run already terminal' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.team_runs
     SET status       = 'cancelled',
         completed_at = now()
   WHERE id = p_team_run_id;

  IF v_run.workflow_run_id IS NOT NULL THEN
    UPDATE lenses.workflow_runs
       SET status       = 'cancelled',
           completed_at = COALESCE(completed_at, now())
     WHERE id = v_run.workflow_run_id
       AND status NOT IN ('completed','failed','cancelled');
  END IF;

  INSERT INTO agents.agent_run_events (
    team_run_id,
    event_type,
    payload,
    occurred_at
  ) VALUES (
    p_team_run_id,
    'run_cancelled',
    jsonb_build_object(
      'cancelled_by',     auth.uid(),
      'previous_status',  v_run.status
    ),
    now()
  );

  INSERT INTO agents.policy_evaluations (
    ai_lenser_id,
    team_run_id,
    evaluation_point,
    policy_type,
    verdict,
    reason,
    context,
    evaluated_at
  ) VALUES (
    v_run.ai_lenser_id,
    p_team_run_id,
    'pre_run',
    'kill_switch',
    'deny',
    'run cancelled by owner',
    jsonb_build_object('cancelled_by', auth.uid()),
    now()
  );
END;
$$;


ALTER FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer DEFAULT 0) RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_run            lenses.workflow_runs%ROWTYPE;
  v_projected      integer;
  v_pending        integer := GREATEST(COALESCE(p_pending_credits, 0), 0);
  v_cost_metadata  jsonb;
BEGIN
  SELECT * INTO v_run
  FROM lenses.workflow_runs
  WHERE id = p_run_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN FALSE;
  END IF;

  -- Never cancel already-terminal runs.
  IF v_run.status IN ('completed', 'failed', 'cancelled', 'timed_out') THEN
    RETURN FALSE;
  END IF;

  IF v_run.budget_credits IS NULL THEN
    RETURN FALSE;
  END IF;

  v_projected := COALESCE(v_run.spent_credits, 0) + v_pending;

  IF v_projected <= v_run.budget_credits THEN
    RETURN FALSE;
  END IF;

  v_cost_metadata := COALESCE(v_run.cost_metadata, '{}'::jsonb)
    || jsonb_build_object(
      'budgetCancellation', jsonb_build_object(
        'cancelledAt', now(),
        'budgetCredits', v_run.budget_credits,
        'spentCredits', v_run.spent_credits,
        'pendingCredits', v_pending,
        'projectedCredits', v_projected,
        'reason', 'budget_exceeded'
      )
    );

  UPDATE lenses.workflow_runs
  SET
    status       = 'cancelled',
    completed_at = now(),
    run_worker_id = NULL,
    cost_metadata = v_cost_metadata
  WHERE id = p_run_id;

  -- Cascade cancellation to any still-running node results so the timeline
  -- view does not keep spinners going forever.
  UPDATE lenses.workflow_node_results
  SET status       = 'cancelled',
      completed_at = now(),
      error_message = COALESCE(error_message, 'budget_exceeded')
  WHERE run_id = p_run_id
    AND status IN ('pending', 'awaiting_dependency', 'queued', 'running', 'streaming', 'retrying');

  -- Append an event so SSE clients see the cancellation immediately.
  INSERT INTO lenses.workflow_run_events (run_id, event_id, type, payload)
  SELECT
    p_run_id,
    COALESCE(MAX(e.event_id), 0) + 1,
    'run.cancelled',
    jsonb_build_object(
      'runId', p_run_id,
      'reason', 'budget_exceeded',
      'budgetCredits', v_run.budget_credits,
      'spentCredits', v_run.spent_credits,
      'pendingCredits', v_pending
    )
  FROM lenses.workflow_run_events e
  WHERE e.run_id = p_run_id;

  RETURN TRUE;
END;
$$;


ALTER FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) IS 'Phase 9 — cancels a run when spent_credits + pendingReservations exceed budget_credits. Cascades to node results and emits run.cancelled event. Invoked periodically by the worker heartbeat loop.';



CREATE OR REPLACE FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_agg public.notification_aggregates;
BEGIN
  -- Look for an open window; lock the row to prevent parallel duplicates.
  SELECT *
  INTO   v_agg
  FROM   public.notification_aggregates
  WHERE  recipient_id      = p_recipient_id
    AND  notification_type = p_type
    AND  entity_id         = p_entity_id
    AND  window_end        > now()
  LIMIT  1
  FOR UPDATE SKIP LOCKED;

  IF FOUND THEN
    -- Within the window: append actor (if new) and suppress new notification.
    IF NOT (p_actor_id = ANY(v_agg.actor_ids)) THEN
      UPDATE public.notification_aggregates
      SET    actor_ids = array_append(actor_ids, p_actor_id)
      WHERE  id        = v_agg.id;
    END IF;
    RETURN false;
  END IF;

  -- No open window: create a new one and allow the notification.
  INSERT INTO public.notification_aggregates
    (recipient_id, notification_type, entity_id, window_start, window_end, actor_ids)
  VALUES
    (p_recipient_id, p_type, p_entity_id, now(), now() + p_window, ARRAY[p_actor_id]);

  RETURN true;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_check_and_upsert_aggregate failed: %', SQLERRM;
  RETURN true;  -- fail-open — allow the notification on error
END;
$$;


ALTER FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_check_handle"("p_handle" "text") RETURNS TABLE("verdict" "text", "class_hit" "text", "risk_score" smallint, "reason_codes" "text"[], "is_available" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT
    v.verdict,
    v.class_hit,
    v.risk_score,
    v.reason_codes,
    (v.verdict = 'allow') AS is_available
  FROM identity_gov.fn_validate_handle(p_handle) v;
$$;


ALTER FUNCTION "public"."fn_check_handle"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_check_handle"("p_handle" "text") IS 'Public read-through handle availability check. Safe for debounced UI calls (STABLE, no writes). Granted to authenticated and anon; results are advisory — fn_claim_handle on the write path performs the authoritative check.';



CREATE OR REPLACE FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") RETURNS "battles"."media_quality_results"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_submission  RECORD;
  v_template_id UUID;
  v_modality    TEXT;
  v_meta        JSONB;
  v_rule        RECORD;
  v_violations  TEXT[] := ARRAY[]::TEXT[];
  v_width       INT;
  v_height      INT;
  v_duration    INT;
  v_aspect      TEXT;
  v_size_mb     NUMERIC;
  v_result      battles.media_quality_results%ROWTYPE;
BEGIN
  SELECT s.*, b.template_id AS battle_template_id
    INTO v_submission
    FROM battles.submissions s
    JOIN battles.battles b ON b.id = s.battle_id
   WHERE s.id = p_submission_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'submission_not_found' USING ERRCODE = '42501';
  END IF;

  v_template_id := v_submission.battle_template_id;
  v_meta        := COALESCE(v_submission.metadata->'media', '{}'::jsonb);
  v_modality    := COALESCE(v_meta->>'modality', v_submission.metadata->>'output_modality');

  IF v_modality IS NULL OR v_modality NOT IN ('image','video','audio') THEN
    RAISE EXCEPTION 'unknown_modality: %', COALESCE(v_modality, '<null>')
      USING ERRCODE = '22023';
  END IF;

  -- Lookup the rule. Absence of a rule = pass (no floor declared).
  -- NOTE: a plpgsql record-typed `v_rule IS NOT NULL` is only true when EVERY
  -- column is non-NULL — wrong test for a row with optional fields. Use FOUND
  -- so we only enforce when a row was actually selected.
  SELECT * INTO v_rule
    FROM battles.media_quality_rules
   WHERE template_id = v_template_id
     AND modality    = v_modality;

  IF FOUND THEN
    v_width    := NULLIF(v_meta->>'width', '')::INT;
    v_height   := NULLIF(v_meta->>'height', '')::INT;
    v_duration := NULLIF(v_meta->>'duration_seconds', '')::INT;
    v_aspect   := v_meta->>'aspect_ratio';
    v_size_mb  := NULLIF(v_meta->>'size_mb', '')::NUMERIC;

    IF v_rule.min_width IS NOT NULL
       AND (v_width IS NULL OR v_width < v_rule.min_width) THEN
      v_violations := array_append(v_violations,
        format('min_width:%s<%s', COALESCE(v_width::text, 'null'), v_rule.min_width));
    END IF;
    IF v_rule.min_height IS NOT NULL
       AND (v_height IS NULL OR v_height < v_rule.min_height) THEN
      v_violations := array_append(v_violations,
        format('min_height:%s<%s', COALESCE(v_height::text, 'null'), v_rule.min_height));
    END IF;
    IF v_rule.max_duration_seconds IS NOT NULL
       AND v_duration IS NOT NULL
       AND v_duration > v_rule.max_duration_seconds THEN
      v_violations := array_append(v_violations,
        format('max_duration_seconds:%s>%s', v_duration, v_rule.max_duration_seconds));
    END IF;
    IF v_rule.required_aspect_ratio IS NOT NULL
       AND v_aspect IS DISTINCT FROM v_rule.required_aspect_ratio THEN
      v_violations := array_append(v_violations,
        format('aspect_ratio:%s!=%s', COALESCE(v_aspect, 'null'), v_rule.required_aspect_ratio));
    END IF;
    IF v_rule.max_file_size_mb IS NOT NULL
       AND v_size_mb IS NOT NULL
       AND v_size_mb > v_rule.max_file_size_mb THEN
      v_violations := array_append(v_violations,
        format('max_file_size_mb:%s>%s', v_size_mb, v_rule.max_file_size_mb));
    END IF;
  END IF;

  INSERT INTO battles.media_quality_results (
    submission_id, passed, violations, checked_at
  ) VALUES (
    p_submission_id,
    cardinality(v_violations) = 0,
    to_jsonb(v_violations),
    now()
  )
  ON CONFLICT (submission_id) DO UPDATE SET
    passed     = EXCLUDED.passed,
    violations = EXCLUDED.violations,
    checked_at = EXCLUDED.checked_at
  RETURNING * INTO v_result;

  RETURN v_result;
END $$;


ALTER FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'public'
    AS $$
DECLARE
  v_recent_count INT;
BEGIN
  SELECT COUNT(*)
    INTO v_recent_count
    FROM battles.votes
   WHERE voter_lenser_id = p_voter_id
     AND created_at >= now() - INTERVAL '60 seconds';

  IF v_recent_count >= 3 THEN
    RAISE EXCEPTION 'rate_limit_exceeded'
      USING ERRCODE = 'P0001',
            DETAIL  = 'You are voting too fast. Please wait before casting another vote.',
            HINT    = 'rate_limit_exceeded';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") IS 'CA: Raises P0001 rate_limit_exceeded when voter has cast ≥3 votes in the last 60s.';



CREATE OR REPLACE FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_eligibility text;
  v_type        text;
  v_onboarding  text;
BEGIN
  SELECT voter_eligibility INTO v_eligibility
  FROM battles.battles
  WHERE id = p_battle_id;

  IF NOT FOUND THEN RETURN false; END IF;
  IF v_eligibility = 'open' THEN RETURN true; END IF;

  SELECT type, onboarding_step INTO v_type, v_onboarding
  FROM lensers.profiles
  WHERE id = p_lenser_id;

  IF NOT FOUND THEN RETURN false; END IF;

  IF    v_eligibility = 'human_only'      THEN RETURN v_type = 'human';
  ELSIF v_eligibility = 'ai_only'         THEN RETURN v_type = 'ai';
  ELSIF v_eligibility = 'verified_lenser' THEN RETURN v_onboarding = 'completed';
  END IF;

  RETURN true;
END;
$$;


ALTER FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_check_webhook_signing_secret"() RETURNS TABLE("is_set" boolean, "length_bytes" integer, "strict_mode" boolean)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
DECLARE
  v_secret text := current_setting('app.webhook_signing_secret', true);
  v_strict text := current_setting('app.webhook_strict_signing', true);
BEGIN
  is_set := v_secret IS NOT NULL AND length(v_secret) > 0;
  length_bytes := COALESCE(length(v_secret), 0);
  strict_mode := COALESCE(lower(v_strict) = 'true', false);
  RETURN NEXT;
END;
$$;


ALTER FUNCTION "public"."fn_check_webhook_signing_secret"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer DEFAULT 60000, "p_max_claims" integer DEFAULT 1) RETURNS TABLE("run_id" "uuid", "workflow_id" "uuid", "parent_run_id" "uuid", "recursion_depth" integer, "previous_status" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_threshold interval := make_interval(secs => GREATEST(p_stale_after_ms, 0)::numeric / 1000.0);
BEGIN
  IF p_worker_id IS NULL OR length(p_worker_id) = 0 THEN
    RAISE EXCEPTION 'fn_claim_stale_workflow_run: p_worker_id is required'
      USING ERRCODE = '22023';
  END IF;

  RETURN QUERY
  WITH claimable AS (
    SELECT r.id, r.workflow_id, r.parent_run_id, r.recursion_depth, r.status
    FROM lenses.workflow_runs r
    WHERE r.status IN ('queued', 'running', 'streaming', 'recovered')
      AND (
            r.heartbeat_at IS NULL
        OR  r.heartbeat_at < (now() - v_threshold)
      )
      -- If another worker already owns the run AND is heartbeating, we
      -- would have filtered it above. The remaining `run_worker_id IS NOT
      -- NULL` rows are genuinely stale.
    ORDER BY COALESCE(r.heartbeat_at, r.started_at, r.created_at) ASC
    LIMIT GREATEST(p_max_claims, 1)
    FOR UPDATE SKIP LOCKED
  ),
  claimed AS (
    UPDATE lenses.workflow_runs r
    SET status        = 'recovered',
        run_worker_id = p_worker_id,
        heartbeat_at  = now()
    FROM claimable c
    WHERE r.id = c.id
    RETURNING r.id, r.workflow_id, r.parent_run_id, r.recursion_depth, c.status AS previous_status
  )
  SELECT
    claimed.id,
    claimed.workflow_id,
    claimed.parent_run_id,
    claimed.recursion_depth,
    claimed.previous_status
  FROM claimed;
END;
$$;


ALTER FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) IS 'Phase 7 — Atomic claim of stale workflow runs for the crash-recovery loop. Uses FOR UPDATE SKIP LOCKED; multiple workers can race safely. Sets status=recovered and stamps run_worker_id + heartbeat_at in one transaction. Returns zero rows when nothing is claimable.';



CREATE OR REPLACE FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers', 'content'
    AS $$
  SELECT lenses.fn_clone_lens(p_source_lens_id, p_version_id);
$$;


ALTER FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_caller_id uuid;
  v_source    lenses.workflows%ROWTYPE;
  v_new_id    uuid := gen_random_uuid();
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT wf.* INTO v_source
  FROM lenses.workflows wf
  WHERE wf.id = p_source_workflow_id;

  IF NOT FOUND OR v_source.visibility::text != 'public' THEN
    RAISE EXCEPTION 'Workflow not found or not public';
  END IF;

  -- Create new workflow (private, owned by caller)
  INSERT INTO lenses.workflows (id, lenser_id, title, description, visibility, battle_count, fork_count)
  VALUES (
    v_new_id,
    v_caller_id,
    'Fork of ' || v_source.title,
    v_source.description,
    'private',
    0,
    0
  );

  UPDATE lenses.workflows wf2
  SET parent_workflow_id = p_source_workflow_id
  WHERE wf2.id = v_new_id;

  -- Copy nodes with config, building old->new id map
  CREATE TEMP TABLE _clone_node_map (old_id uuid, new_id uuid) ON COMMIT DROP;

  INSERT INTO _clone_node_map (old_id, new_id)
  SELECT n.id, gen_random_uuid()
  FROM lenses.workflow_nodes n
  WHERE n.workflow_id = p_source_workflow_id;

  INSERT INTO lenses.workflow_nodes (id, workflow_id, lens_id, version_id, position_x, position_y, label, ordinal, config)
  SELECT m.new_id, v_new_id, n.lens_id, n.version_id, n.position_x, n.position_y, n.label, n.ordinal, n.config
  FROM lenses.workflow_nodes n
  JOIN _clone_node_map m ON m.old_id = n.id;

  -- Copy edges, translating old node ids to new ids
  INSERT INTO lenses.workflow_edges (id, workflow_id, source_node_id, target_node_id, source_output_key, target_param_label)
  SELECT
    gen_random_uuid(),
    v_new_id,
    ms.new_id,
    mt.new_id,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_edges e
  JOIN _clone_node_map ms ON ms.old_id = e.source_node_id
  JOIN _clone_node_map mt ON mt.old_id = e.target_node_id
  WHERE e.workflow_id = p_source_workflow_id;

  -- Increment fork_count on source
  UPDATE lenses.workflows src
  SET fork_count = src.fork_count + 1
  WHERE src.id = p_source_workflow_id;

  -- Award XP (best-effort)
  BEGIN
    PERFORM xp.apply(
      v_caller_id,
      'WORKFLOW_FORKED',
      'content'::xp.source_enum,
      'workflow',
      v_new_id
    );
    PERFORM xp.apply(
      v_source.lenser_id,
      'WORKFLOW_FORK_RECEIVED',
      'content'::xp.source_enum,
      'workflow',
      p_source_workflow_id
    );
  EXCEPTION WHEN OTHERS THEN
    NULL;
  END;

  RETURN QUERY
    SELECT
      w.id, w.lenser_id, w.title, w.description, w.visibility::text,
      w.battle_count, w.reaction_totals, w.fork_count, w.parent_workflow_id,
      w.created_at, w.updated_at
    FROM lenses.workflows w
    WHERE w.id = v_new_id;
END;
$$;


ALTER FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") IS 'Clones a public workflow including nodes (with config), edges, and XP awards. Returns the new workflow row.';



CREATE OR REPLACE FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_existing_profile_id UUID;
BEGIN
  -- Confirm the caller owns a profile
  SELECT id INTO v_existing_profile_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
  LIMIT 1;

  IF v_existing_profile_id IS NULL THEN
    RAISE EXCEPTION 'PROFILE_NOT_FOUND';
  END IF;

  -- Guard handle uniqueness (raise on conflict so UI can show a clear error)
  IF EXISTS (
    SELECT 1 FROM lensers.profiles
    WHERE handle = lower(p_handle)
      AND id <> v_existing_profile_id
  ) THEN
    RAISE EXCEPTION 'HANDLE_TAKEN';
  END IF;

  UPDATE lensers.profiles
  SET
    handle              = lower(p_handle),
    display_name        = p_display_name,
    onboarding_step     = 2,
    onboarding_completed_at = now(),
    updated_at          = now()
  WHERE id = v_existing_profile_id;
END;
$$;


ALTER FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text" DEFAULT 'completed'::"text", "p_cost_credits" integer DEFAULT 0, "p_error" "text" DEFAULT NULL::"text") RETURNS "agents"."scratchpad_runs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_existing agents.scratchpad_runs;
  v_row      agents.scratchpad_runs;
BEGIN
  SELECT * INTO v_existing FROM agents.scratchpad_runs WHERE id = p_run_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'scratchpad run not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_existing.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF p_status NOT IN ('running', 'completed', 'failed', 'cancelled') THEN
    RAISE EXCEPTION 'invalid status' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.scratchpad_runs
  SET output       = p_output,
      status       = p_status,
      cost_credits = COALESCE(p_cost_credits, 0),
      error        = p_error,
      completed_at = CASE
                       WHEN p_status IN ('completed', 'failed', 'cancelled') THEN now()
                       ELSE completed_at
                     END
  WHERE id = p_run_id
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb" DEFAULT NULL::"jsonb", "p_error" "text" DEFAULT NULL::"text", "p_cost" numeric DEFAULT NULL::numeric) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_inv agents.tool_invocations;
BEGIN
  SELECT * INTO v_inv FROM agents.tool_invocations WHERE id = p_invocation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'invocation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_inv.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF p_status NOT IN ('completed','failed') THEN
    RAISE EXCEPTION 'completion status must be completed or failed' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.tool_invocations
     SET status        = p_status,
         output        = COALESCE(p_output, output),
         error         = COALESCE(p_error, error),
         cost_estimate = COALESCE(p_cost, cost_estimate),
         completed_at  = now()
   WHERE id = p_invocation_id;
END;
$$;


ALTER FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'reputation'
    AS $$
DECLARE
  v_battle           battles.battles%ROWTYPE;
  v_winner_lenser_id uuid;
  v_loser_lenser_id  uuid;
  v_winner_score     numeric(10,4);
  v_loser_score      numeric(10,4);
  v_winner_new       numeric(10,4);
  v_loser_new        numeric(10,4);
  v_e_winner         numeric;
  v_e_loser          numeric;
  v_k_factor         constant integer := 32;
  v_default_score    constant numeric := 1000;
  v_is_draw          boolean := false;
BEGIN
  -- Idempotency guard: insert log row first; bail if a row already exists.
  INSERT INTO reputation.elo_battle_log (battle_id, k_factor)
  VALUES (p_battle_id, v_k_factor)
  ON CONFLICT (battle_id) DO NOTHING;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('skipped', true, 'reason', 'already_computed', 'battle_id', p_battle_id);
  END IF;

  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN
    -- Battle vanished after we logged; clean up to allow a future retry.
    DELETE FROM reputation.elo_battle_log WHERE battle_id = p_battle_id;
    RAISE EXCEPTION 'Battle % not found', p_battle_id USING ERRCODE = '42704';
  END IF;

  IF v_battle.status <> 'closed' THEN
    DELETE FROM reputation.elo_battle_log WHERE battle_id = p_battle_id;
    RAISE EXCEPTION 'Battle % is in status %, must be closed before ELO compute',
      p_battle_id, v_battle.status USING ERRCODE = '22023';
  END IF;

  -- Resolve winner/loser. NULL winner_contender_id ⇒ tie.
  IF v_battle.winner_contender_id IS NULL THEN
    v_is_draw := true;
    SELECT contender_ref_id
      INTO v_winner_lenser_id
    FROM battles.contenders
    WHERE battle_id = p_battle_id AND slot = 'A'
    LIMIT 1;

    SELECT contender_ref_id
      INTO v_loser_lenser_id
    FROM battles.contenders
    WHERE battle_id = p_battle_id AND slot = 'B'
    LIMIT 1;
  ELSE
    SELECT contender_ref_id
      INTO v_winner_lenser_id
    FROM battles.contenders
    WHERE id = v_battle.winner_contender_id;

    SELECT contender_ref_id
      INTO v_loser_lenser_id
    FROM battles.contenders
    WHERE battle_id = p_battle_id
      AND id <> v_battle.winner_contender_id
    LIMIT 1;
  END IF;

  IF v_winner_lenser_id IS NULL OR v_loser_lenser_id IS NULL THEN
    DELETE FROM reputation.elo_battle_log WHERE battle_id = p_battle_id;
    RAISE EXCEPTION 'Battle % has insufficient contenders for ELO compute', p_battle_id
      USING ERRCODE = '22023';
  END IF;

  -- Read current ELO scores; default 1000 if no row.
  SELECT score INTO v_winner_score
    FROM reputation.lenser_scores
   WHERE lenser_id = v_winner_lenser_id AND score_type = 'elo';
  v_winner_score := COALESCE(v_winner_score, v_default_score);

  SELECT score INTO v_loser_score
    FROM reputation.lenser_scores
   WHERE lenser_id = v_loser_lenser_id AND score_type = 'elo';
  v_loser_score := COALESCE(v_loser_score, v_default_score);

  -- Expected scores.
  v_e_winner := 1.0 / (1.0 + power(10.0, (v_loser_score - v_winner_score) / 400.0));
  v_e_loser  := 1.0 / (1.0 + power(10.0, (v_winner_score - v_loser_score) / 400.0));

  IF v_is_draw THEN
    v_winner_new := v_winner_score + v_k_factor * (0.5 - v_e_winner);
    v_loser_new  := v_loser_score  + v_k_factor * (0.5 - v_e_loser);
  ELSE
    v_winner_new := v_winner_score + v_k_factor * (1.0 - v_e_winner);
    v_loser_new  := v_loser_score  + v_k_factor * (0.0 - v_e_loser);
  END IF;

  -- Persist updated scores.
  INSERT INTO reputation.lenser_scores (lenser_id, score_type, score, computed_at)
  VALUES (v_winner_lenser_id, 'elo', v_winner_new, now())
  ON CONFLICT (lenser_id, score_type) DO UPDATE
    SET score = EXCLUDED.score, computed_at = now();

  INSERT INTO reputation.lenser_scores (lenser_id, score_type, score, computed_at)
  VALUES (v_loser_lenser_id, 'elo', v_loser_new, now())
  ON CONFLICT (lenser_id, score_type) DO UPDATE
    SET score = EXCLUDED.score, computed_at = now();

  -- Backfill the audit log row inserted at top of function.
  UPDATE reputation.elo_battle_log
  SET winner_lenser_id    = v_winner_lenser_id,
      loser_lenser_id     = v_loser_lenser_id,
      is_draw             = v_is_draw,
      winner_score_before = v_winner_score,
      winner_score_after  = v_winner_new,
      loser_score_before  = v_loser_score,
      loser_score_after   = v_loser_new
  WHERE battle_id = p_battle_id;

  RETURN jsonb_build_object(
    'battle_id',           p_battle_id,
    'is_draw',             v_is_draw,
    'winner_lenser_id',    v_winner_lenser_id,
    'loser_lenser_id',     v_loser_lenser_id,
    'winner_score_before', v_winner_score,
    'winner_score_after',  v_winner_new,
    'loser_score_before',  v_loser_score,
    'loser_score_after',   v_loser_new,
    'k_factor',            v_k_factor
  );
END;
$$;


ALTER FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") IS 'Phase O3: K=32 per-battle ELO update. Reads winner/loser from battles.contenders, applies the standard formula, upserts reputation.lenser_scores (score_type=elo), and logs the operation in reputation.elo_battle_log for idempotency.';



CREATE OR REPLACE FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) RETURNS "jsonb"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'public', 'connectors'
    AS $$
BEGIN
    PERFORM connectors.fn_assert_scope(p_granted, p_required);
    RETURN jsonb_build_object('ok', true, 'required', p_required);
END
$$;


ALTER FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_workspace_id uuid := connectors.fn_active_workspace_id();
    v_user_id      uuid := auth.uid();
    v_connector_id uuid;
    v_token_raw    text;
    v_token_hash   text;
    v_token_prefix text;
BEGIN
    IF v_workspace_id IS NULL THEN
        RAISE EXCEPTION 'no active workspace for caller'
              USING ERRCODE = '42501';
    END IF;

    PERFORM connectors.fn_assert_known_scopes(p_scopes);
    IF cardinality(p_scopes) = 0 THEN
        RAISE EXCEPTION 'p_scopes must be non-empty' USING ERRCODE = '22023';
    END IF;

    INSERT INTO connectors.connectors (workspace_id, slug, name, description, created_by)
    VALUES (v_workspace_id, p_slug, p_name, NULLIF(p_description, ''), v_user_id)
    RETURNING id INTO v_connector_id;

    -- 32 random bytes → ~43-char base64url; "lf_" prefix marks origin.
    v_token_raw    := 'lf_' || encode(gen_random_bytes(32), 'base64');
    v_token_raw    := replace(replace(replace(v_token_raw, '+', '-'), '/', '_'), '=', '');
    v_token_hash   := connectors.fn_hash_token(v_token_raw);
    v_token_prefix := substring(v_token_raw FROM 1 FOR 11);

    INSERT INTO connectors.connector_tokens (connector_id, token_hash, token_prefix, scopes)
    VALUES (v_connector_id, v_token_hash, v_token_prefix, p_scopes);

    RETURN jsonb_build_object(
        'slug',          p_slug,
        'name',          p_name,
        'scopes',        p_scopes,
        'service_token', v_token_raw,
        'token_prefix',  v_token_prefix
    );
END
$$;


ALTER FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_get"("p_slug" "text") RETURNS TABLE("slug" "text", "name" "text", "description" "text", "scopes" "text"[], "is_active" boolean, "kind" "text", "created_at" timestamp with time zone, "last_used_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
    SELECT c.slug,
           c.name,
           c.description,
           COALESCE(
               (SELECT t.scopes
                  FROM connectors.connector_tokens t
                 WHERE t.connector_id = c.id
                   AND t.revoked_at IS NULL
                 ORDER BY t.created_at DESC
                 LIMIT 1),
               ARRAY[]::text[]
           ) AS scopes,
           c.is_active,
           c.kind,
           c.created_at,
           c.last_used_at
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug
     LIMIT 1
$$;


ALTER FUNCTION "public"."fn_connector_get"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_remove"("p_slug" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_connector_id uuid;
BEGIN
    SELECT c.id INTO v_connector_id
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug;

    IF v_connector_id IS NULL THEN
        RAISE EXCEPTION 'connector not found: %', p_slug USING ERRCODE = 'P0002';
    END IF;

    UPDATE connectors.connectors
       SET is_active = false
     WHERE id = v_connector_id;

    UPDATE connectors.connector_tokens
       SET revoked_at = now()
     WHERE connector_id = v_connector_id
       AND revoked_at IS NULL;
END
$$;


ALTER FUNCTION "public"."fn_connector_remove"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_rotate"("p_slug" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_connector_id uuid;
    v_scopes       text[];
    v_token_raw    text;
    v_token_hash   text;
    v_token_prefix text;
BEGIN
    SELECT c.id INTO v_connector_id
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug
       AND c.is_active = true;

    IF v_connector_id IS NULL THEN
        RAISE EXCEPTION 'connector not found or inactive: %', p_slug USING ERRCODE = 'P0002';
    END IF;

    SELECT t.scopes INTO v_scopes
      FROM connectors.connector_tokens t
     WHERE t.connector_id = v_connector_id
       AND t.revoked_at IS NULL
     ORDER BY t.created_at DESC
     LIMIT 1;

    IF v_scopes IS NULL THEN
        RAISE EXCEPTION 'no active token to rotate for connector: %', p_slug USING ERRCODE = 'P0002';
    END IF;

    UPDATE connectors.connector_tokens
       SET revoked_at = now()
     WHERE connector_id = v_connector_id
       AND revoked_at IS NULL;

    v_token_raw    := 'lf_' || encode(gen_random_bytes(32), 'base64');
    v_token_raw    := replace(replace(replace(v_token_raw, '+', '-'), '/', '_'), '=', '');
    v_token_hash   := connectors.fn_hash_token(v_token_raw);
    v_token_prefix := substring(v_token_raw FROM 1 FOR 11);

    INSERT INTO connectors.connector_tokens (connector_id, token_hash, token_prefix, scopes)
    VALUES (v_connector_id, v_token_hash, v_token_prefix, v_scopes);

    RETURN jsonb_build_object(
        'slug',          p_slug,
        'service_token', v_token_raw,
        'token_prefix',  v_token_prefix
    );
END
$$;


ALTER FUNCTION "public"."fn_connector_rotate"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connector_test"("p_slug" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
DECLARE
    v_connector_id uuid;
    v_scopes       text[];
    v_started_at   timestamptz := clock_timestamp();
BEGIN
    SELECT c.id INTO v_connector_id
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
       AND c.slug = p_slug
       AND c.is_active = true;

    IF v_connector_id IS NULL THEN
        RETURN jsonb_build_object('ok', false, 'latency_ms', 0, 'scopes', '[]'::jsonb);
    END IF;

    SELECT t.scopes INTO v_scopes
      FROM connectors.connector_tokens t
     WHERE t.connector_id = v_connector_id
       AND t.revoked_at IS NULL
     ORDER BY t.created_at DESC
     LIMIT 1;

    UPDATE connectors.connectors
       SET last_used_at = now()
     WHERE id = v_connector_id;

    RETURN jsonb_build_object(
        'ok',         v_scopes IS NOT NULL,
        'latency_ms', EXTRACT(MILLISECOND FROM clock_timestamp() - v_started_at)::int,
        'scopes',     COALESCE(v_scopes, ARRAY[]::text[])
    );
END
$$;


ALTER FUNCTION "public"."fn_connector_test"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_connectors_list"() RETURNS TABLE("slug" "text", "name" "text", "description" "text", "scopes" "text"[], "is_active" boolean, "kind" "text", "created_at" timestamp with time zone, "last_used_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'connectors'
    AS $$
    SELECT c.slug,
           c.name,
           c.description,
           COALESCE(
               (SELECT t.scopes
                  FROM connectors.connector_tokens t
                 WHERE t.connector_id = c.id
                   AND t.revoked_at IS NULL
                 ORDER BY t.created_at DESC
                 LIMIT 1),
               ARRAY[]::text[]
           ) AS scopes,
           c.is_active,
           c.kind,
           c.created_at,
           c.last_used_at
      FROM connectors.connectors c
     WHERE c.workspace_id = connectors.fn_active_workspace_id()
     ORDER BY c.created_at DESC
$$;


ALTER FUNCTION "public"."fn_connectors_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[] DEFAULT '{}'::"uuid"[]) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
  v_thread_id uuid;
  v_lang      text := 'en';
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthenticated or no active lenser profile';
  END IF;

  SELECT coalesce(lp.language, 'en')
  INTO v_lang
  FROM lensers.preferences lp
  WHERE lp.lenser_id = v_lenser_id;

  INSERT INTO content.threads (lenser_id, visibility)
  VALUES (v_lenser_id, p_visibility)
  RETURNING id INTO v_thread_id;

  INSERT INTO content.entity_translations (
    entity_type, entity_id, language_code, is_original, title, content
  ) VALUES (
    'thread', v_thread_id, v_lang, true, p_title, p_content
  );

  IF p_tag_ids IS NOT NULL AND array_length(p_tag_ids, 1) > 0 THEN
    INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
    SELECT 'thread', v_thread_id, unnest(p_tag_ids)
    ON CONFLICT DO NOTHING;
  END IF;

  RETURN v_thread_id;
END;
$$;


ALTER FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  -- Verify the tag is public
  IF NOT EXISTS (
    SELECT 1 FROM content.tags
    WHERE id = p_tag_id AND visibility = 'public'::"content"."tag_visibility_enum"
  ) THEN
    RAISE EXCEPTION 'Tag not found';
  END IF;

  INSERT INTO lensers.tag_follows (lenser_id, tag_id)
  VALUES (v_lenser_id, p_tag_id)
  ON CONFLICT (lenser_id, tag_id) DO NOTHING;

  RETURN jsonb_build_object('following', true);
END;
$$;


ALTER FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") RETURNS TABLE("tag_id" "uuid", "slug" "text", "name" "text", "followed_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content'
    AS $$
  SELECT
    t.id,
    t.slug,
    COALESCE(
      tt_pref.name,   -- preferred language first
      tt_en.name,     -- English fallback
      t.slug          -- final fallback: slug
    ) AS name,
    tf.created_at AS followed_at
  FROM lensers.tag_follows tf
  JOIN content.tags t ON t.id = tf.tag_id
  LEFT JOIN content.tag_translations tt_en
    ON tt_en.tag_id = t.id AND tt_en.language_code = 'en'
  LEFT JOIN content.tag_translations tt_pref
    ON tt_pref.tag_id = t.id
    AND tt_pref.language_code = (
      SELECT lp.language
      FROM lensers.preferences lp
      WHERE lp.lenser_id = p_lenser_id
    )
  WHERE tf.lenser_id = p_lenser_id
    AND t.visibility = 'public'::"content"."tag_visibility_enum"
  ORDER BY tf.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "description" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
WITH current_lenser AS (
  SELECT p.id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.id = p_lenser_id
  LIMIT 1
),
followed_authors AS (
  SELECT DISTINCT r.target_profile_id AS lenser_id
  FROM lensers.relationships r
  WHERE r.source_profile_id = (SELECT id FROM current_lenser)
    AND r.status = 'accepted'
)
SELECT
  v.id,
  v.author_profile,
  v.tags,
  v.reaction_totals,
  v.title,
  v.description,
  v.created_at
FROM public.vw_lenses_public v
JOIN followed_authors fa ON fa.lenser_id = v.lenser_id
ORDER BY v.created_at DESC
LIMIT GREATEST(LEAST(p_limit, 50), 0)
OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Returns public lenses authored by lensers the current viewer follows. SECURITY DEFINER and auth-scoped.';



CREATE OR REPLACE FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "content" "text", "reply_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
WITH current_lenser AS (
  SELECT p.id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.id = p_lenser_id
  LIMIT 1
),
followed_authors AS (
  SELECT DISTINCT r.target_profile_id AS lenser_id
  FROM lensers.relationships r
  WHERE r.source_profile_id = (SELECT id FROM current_lenser)
    AND r.status = 'accepted'
)
SELECT
  v.id,
  log(GREATEST(1,
    2.0 * COALESCE(v.like_count, 0)
    + 3.0 * COALESCE(v.reply_count, 0)
    + 0.5 * COALESCE(v.view_count, 0)
  )) / pow(EXTRACT(epoch FROM (now() - v.created_at)) / 3600.0 + 2, 1.5) AS hot_score,
  et.language_code AS primary_language,
  v.author_profile,
  v.tags,
  v.reaction_totals,
  v.title,
  v.content,
  v.reply_count,
  v.created_at
FROM public.vw_content_threads_public v
JOIN followed_authors fa ON fa.lenser_id = v.lenser_id
LEFT JOIN content.entity_translations et
  ON et.entity_id = v.id
 AND et.entity_type = 'thread'::content.entity_type_enum
 AND et.is_original = true
ORDER BY v.created_at DESC
LIMIT GREATEST(LEAST(p_limit, 50), 0)
OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Returns public threads authored by lensers the current viewer follows. SECURITY DEFINER and auth-scoped.';



CREATE OR REPLACE FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text" DEFAULT 'newest'::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "content"."visibility_enum", "title" "text", "description" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "copy_count" integer, "like_count" integer, "saved_count" integer, "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  WITH matched_ids AS (
    SELECT DISTINCT tm.entity_id AS prompt_id
    FROM content.tag_map tm
    JOIN content.tags tg ON tg.id = tm.tag_id AND tg.slug = p_tag_slug
    WHERE tm.entity_type = 'lens'
    LIMIT 1000
  )
  SELECT
    v.id, v.lenser_id, v.visibility, v.title, v.description,
    v.author_profile, v.reaction_totals, v.copy_count, v.like_count,
    v.saved_count, v.tags, v.created_at
  FROM matched_ids m
  JOIN public.vw_lenses_public v ON v.id = m.prompt_id
  ORDER BY
    CASE WHEN p_sort = 'newest' THEN v.created_at END DESC,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.copy_count END DESC NULLS LAST,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.like_count END DESC NULLS LAST
  LIMIT  LEAST(p_limit, 50)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "personal_score" double precision, "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "description" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
WITH
  current_lenser AS (
    SELECT p.id, COALESCE(lp.language, 'en') AS preferred_language, p.user_id
    FROM lensers.profiles p
    LEFT JOIN lensers.preferences lp ON lp.lenser_id = p.id
    WHERE p.user_id = auth.uid()
    LIMIT 1
  ),
  interest_tags AS (
    SELECT tf.tag_id
    FROM lensers.tag_follows tf
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT xcl.equivalent_tag_id
    FROM lensers.tag_follows tf
    JOIN content.vw_tag_cross_lang xcl ON xcl.source_tag_id = tf.tag_id
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT DISTINCT tm.tag_id
    FROM content.reactions r
    JOIN lenses.lenses   pt2 ON pt2.id = r.entity_id
    JOIN content.tag_map tm  ON tm.entity_id = pt2.id AND tm.entity_type = 'lens'
    WHERE r.entity_type = 'lens'
      AND r.lenser_id   = (SELECT user_id FROM current_lenser)
      AND r.created_at  > now() - interval '30 days'
  ),
  candidates AS (
    SELECT pt.id, pt.created_at, pt.lenser_id
    FROM lenses.lenses pt
    WHERE pt.visibility = 'public'
      AND pt.status     = 'published'
      AND (SELECT id FROM current_lenser) IS NOT NULL
    ORDER BY pt.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS prompt_id,
      COUNT(*) FILTER (WHERE r.reaction = 'copy':: content.reaction_enum) AS copy_count,
      COUNT(*) FILTER (WHERE r.reaction = 'like':: content.reaction_enum) AS like_count,
      COUNT(*) FILTER (WHERE r.reaction = 'saved'::content.reaction_enum) AS saved_count
    FROM content.reactions r
    WHERE r.entity_type = 'lens'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  preliminary_scores AS (
    SELECT
      c.id,
      c.lenser_id,
      et.language_code AS primary_language,
      log(GREATEST(1,
        4.0 * COALESCE(r.copy_count,  0)
      + 2.0 * COALESCE(r.like_count,  0)
      + 1.0 * COALESCE(r.saved_count, 0)
      )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5) AS hot_score,
      (
        0.30 * COALESCE((
          SELECT COUNT(*)::float / GREATEST((SELECT COUNT(*) FROM interest_tags), 1)
          FROM content.tag_map tm
          JOIN interest_tags it ON it.tag_id = tm.tag_id
          WHERE tm.entity_type = 'lens' AND tm.entity_id = c.id
        ), 0.0)
        + 0.25 * CASE
            WHEN et.language_code = (SELECT preferred_language FROM current_lenser) THEN 1.0
            ELSE 0.0
          END
        + 0.20 * LEAST(
            log(GREATEST(1,
              4.0 * COALESCE(r.copy_count,  0)
            + 2.0 * COALESCE(r.like_count,  0)
            + 1.0 * COALESCE(r.saved_count, 0)
            )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5)
            / 2.0, 1.0)
        + 0.10 * CASE WHEN fa.target_profile_id IS NOT NULL THEN 1.0 ELSE 0.0 END
      ) AS preliminary_score
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.prompt_id = c.id
    LEFT JOIN content.entity_translations et
           ON et.entity_id   = c.id
          AND et.entity_type = 'lens'
          AND et.is_original = true
    LEFT JOIN lensers.relationships fa
           ON fa.source_profile_id = (SELECT id FROM current_lenser)
          AND fa.target_profile_id = c.lenser_id
          AND fa.status = 'accepted'
    WHERE c.id NOT IN (
      SELECT target_id FROM content.reports
      WHERE target_type = 'lens'::content.entity_type_enum
      GROUP BY target_id HAVING COUNT(DISTINCT reporter_id) >= 3
    )
    ORDER BY preliminary_score DESC
    LIMIT LEAST(p_limit, 50) * 2
  ),
  candidate_scores AS (
    SELECT
      ps.id,
      ps.primary_language,
      ps.hot_score,
      (
        ps.preliminary_score
        + 0.15 * LEAST(COALESCE(ls.lenser_score, 0.0) / 5.0, 1.0)
      ) AS personal_score
    FROM preliminary_scores ps
    LEFT JOIN lensers.vw_lensers_score ls ON ls.lenser_id = ps.lenser_id
    ORDER BY personal_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT
  v.id, c.personal_score, c.hot_score, c.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, v.description, v.created_at
FROM candidate_scores c
JOIN public.vw_lenses_public v ON v.id = c.id
ORDER BY c.personal_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "personal_score" double precision, "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "content" "text", "reply_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
WITH
  current_lenser AS (
    SELECT p.id, COALESCE(lp.language, 'en') AS preferred_language, p.user_id
    FROM lensers.profiles p
    LEFT JOIN lensers.preferences lp ON lp.lenser_id = p.id
    WHERE p.user_id = auth.uid()
    LIMIT 1
  ),
  interest_tags AS (
    SELECT tf.tag_id
    FROM lensers.tag_follows tf
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT xcl.equivalent_tag_id
    FROM lensers.tag_follows tf
    JOIN content.vw_tag_cross_lang xcl ON xcl.source_tag_id = tf.tag_id
    WHERE tf.lenser_id = (SELECT id FROM current_lenser)
    UNION
    SELECT DISTINCT tm.tag_id
    FROM content.reactions r
    JOIN content.threads t2 ON t2.id = r.entity_id
    JOIN content.tag_map  tm ON tm.entity_id = t2.id AND tm.entity_type = 'thread'
    WHERE r.entity_type = 'thread'
      AND r.lenser_id   = (SELECT user_id FROM current_lenser)
      AND r.created_at  > now() - interval '30 days'
  ),
  candidates AS (
    SELECT t.id, t.created_at, t.lenser_id, t.reply_count, t.view_count
    FROM content.threads t
    WHERE t.visibility = 'public'
      AND t.status     = 'published'
      AND (SELECT id FROM current_lenser) IS NOT NULL
    ORDER BY t.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS thread_id,
      count(*) FILTER (WHERE r.reaction = 'like'::content.reaction_enum) AS like_count
    FROM content.reactions r
    WHERE r.entity_type = 'thread'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  preliminary_scores AS (
    SELECT
      c.id,
      c.lenser_id,
      c.reply_count,
      et.language_code AS primary_language,
      log(greatest(1,
        2.0 * coalesce(r.like_count, 0)
      + 3.0 * c.reply_count
      + 0.5 * c.view_count
      )) / pow(extract(epoch from (now() - c.created_at)) / 3600.0 + 2, 1.5) AS hot_score,
      (
        0.30 * COALESCE((
          SELECT COUNT(*)::float / GREATEST((SELECT COUNT(*) FROM interest_tags), 1)
          FROM content.tag_map tm
          JOIN interest_tags it ON it.tag_id = tm.tag_id
          WHERE tm.entity_type = 'thread' AND tm.entity_id = c.id
        ), 0.0)
        + 0.25 * CASE
            WHEN et.language_code = (SELECT preferred_language FROM current_lenser) THEN 1.0
            ELSE 0.0
          END
        + 0.20 * LEAST(
            log(greatest(1,
              2.0 * coalesce(r.like_count, 0)
            + 3.0 * c.reply_count
            + 0.5 * c.view_count
            )) / pow(extract(epoch from (now() - c.created_at)) / 3600.0 + 2, 1.5)
            / 2.0, 1.0)
        + 0.10 * CASE WHEN fa.target_profile_id IS NOT NULL THEN 1.0 ELSE 0.0 END
      ) AS preliminary_score
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.thread_id = c.id
    LEFT JOIN content.entity_translations et
           ON et.entity_id   = c.id
          AND et.entity_type = 'thread'
          AND et.is_original = true
    LEFT JOIN lensers.relationships fa
           ON fa.source_profile_id = (SELECT id FROM current_lenser)
          AND fa.target_profile_id = c.lenser_id
          AND fa.status = 'accepted'
    WHERE c.id NOT IN (
      SELECT target_id FROM content.reports
      WHERE target_type = 'thread'::content.entity_type_enum
      GROUP BY target_id HAVING COUNT(DISTINCT reporter_id) >= 3
    )
    ORDER BY preliminary_score DESC
    LIMIT LEAST(p_limit, 50) * 2
  ),
  scored AS (
    SELECT
      ps.id,
      ps.primary_language,
      ps.reply_count,
      ps.hot_score,
      (
        ps.preliminary_score
        + 0.15 * LEAST(COALESCE(ls.lenser_score, 0.0) / 5.0, 1.0)
      ) AS personal_score
    FROM preliminary_scores ps
    LEFT JOIN lensers.vw_lensers_score ls ON ls.lenser_id = ps.lenser_id
    ORDER BY personal_score DESC
    LIMIT  LEAST(p_limit, 50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT v.id, s.personal_score, s.hot_score, s.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, v.content, s.reply_count, v.created_at
FROM scored s
JOIN public.vw_content_threads_public v ON v.id = s.id
ORDER BY s.personal_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "content"."visibility_enum", "title" "text", "description" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "copy_count" integer, "like_count" integer, "saved_count" integer, "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers'
    AS $$
WITH
  candidates AS (
    SELECT pt.id, pt.created_at
    FROM lenses.lenses pt
    WHERE pt.visibility = 'public' AND pt.status = 'published'
    ORDER BY pt.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS prompt_id,
      COUNT(*) FILTER (WHERE r.reaction = 'copy':: content.reaction_enum) AS copy_count,
      COUNT(*) FILTER (WHERE r.reaction = 'like':: content.reaction_enum) AS like_count,
      COUNT(*) FILTER (WHERE r.reaction = 'saved'::content.reaction_enum) AS saved_count
    FROM content.reactions r
    WHERE r.entity_type = 'lens'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  ranked AS (
    SELECT
      c.id,
      log(GREATEST(1,
        4.0 * COALESCE(r.copy_count,  0)
      + 2.0 * COALESCE(r.like_count,  0)
      + 1.0 * COALESCE(r.saved_count, 0)
      )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5) AS hot_score
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.prompt_id = c.id
    ORDER BY hot_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT
  v.id, v.lenser_id, v.visibility, v.title, v.description,
  v.author_profile, v.reaction_totals, v.copy_count, v.like_count,
  v.saved_count, v.tags, v.created_at
FROM ranked rk
JOIN public.vw_lenses_public v ON v.id = rk.id
ORDER BY rk.hot_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text" DEFAULT 'newest'::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "content" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "like_count" integer, "reply_count" integer, "view_count" integer, "visibility" "content"."visibility_enum", "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  WITH matched_ids AS (
    SELECT DISTINCT tm.entity_id AS thread_id
    FROM content.tag_map tm
    JOIN content.tags tg ON tg.id = tm.tag_id AND tg.slug = p_tag_slug
    WHERE tm.entity_type = 'thread'
    LIMIT 1000
  )
  SELECT
    v.id, v.lenser_id, v.title, v.content,
    v.author_profile, v.reaction_totals, v.like_count,
    v.reply_count, v.view_count, v.visibility, v.tags, v.created_at
  FROM matched_ids m
  JOIN public.vw_content_threads_public v ON v.id = m.thread_id
  ORDER BY
    CASE WHEN p_sort = 'newest' THEN v.created_at END DESC,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.like_count END DESC NULLS LAST,
    CASE WHEN p_sort IN ('trending', 'popular') THEN v.reply_count END DESC NULLS LAST
  LIMIT  LEAST(p_limit, 50)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "description" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers'
    AS $$
WITH
  candidates AS (
    SELECT pt.id, pt.created_at
    FROM lenses.lenses pt
    WHERE pt.visibility = 'public' AND pt.status = 'published'
    ORDER BY pt.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS prompt_id,
      COUNT(*) FILTER (WHERE r.reaction = 'copy':: content.reaction_enum) AS copy_count,
      COUNT(*) FILTER (WHERE r.reaction = 'like':: content.reaction_enum) AS like_count,
      COUNT(*) FILTER (WHERE r.reaction = 'saved'::content.reaction_enum) AS saved_count
    FROM content.reactions r
    WHERE r.entity_type = 'lens'
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  scored AS (
    SELECT
      c.id,
      log(GREATEST(1,
        4.0 * COALESCE(r.copy_count,  0)
      + 2.0 * COALESCE(r.like_count,  0)
      + 1.0 * COALESCE(r.saved_count, 0)
      )) / pow(EXTRACT(epoch FROM now() - c.created_at) / 3600.0 + 2, 1.5)
        * CASE WHEN p_lang IS NOT NULL AND et.language_code = p_lang THEN 1.5 ELSE 1.0 END
        AS hot_score,
      et.language_code AS primary_language
    FROM candidates c
    LEFT JOIN reaction_agg r ON r.prompt_id = c.id
    LEFT JOIN content.entity_translations et
           ON et.entity_id   = c.id
          AND et.entity_type = 'lens'
          AND et.is_original = true
    ORDER BY hot_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT v.id, s.hot_score, s.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, v.description, v.created_at
FROM scored s
JOIN public.vw_lenses_public v ON v.id = s.id
ORDER BY s.hot_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "hot_score" double precision, "primary_language" "text", "author_profile" "jsonb", "tags" "jsonb", "reaction_totals" "jsonb", "title" "text", "reply_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
WITH
  candidates AS (
    SELECT t.id, t.created_at, t.reply_count, t.view_count
    FROM content.threads t
    WHERE t.visibility = 'public' AND t.status = 'published'
    ORDER BY t.created_at DESC
    LIMIT 5000
  ),
  reaction_agg AS (
    SELECT r.entity_id AS thread_id,
      count(*) FILTER (WHERE r.reaction = 'like'::content.reaction_enum) AS like_count
    FROM content.reactions r
    WHERE r.entity_type = 'thread'::content.entity_type_enum
      AND r.entity_id IN (SELECT id FROM candidates)
    GROUP BY r.entity_id
  ),
  scored AS (
    SELECT
      c.id,
      log(greatest(1,
        2.0 * coalesce(ra.like_count, 0)
      + 3.0 * c.reply_count
      + 0.5 * c.view_count
      )) / pow(extract(epoch from (now() - c.created_at)) / 3600.0 + 2, 1.5)
        * CASE WHEN p_lang IS NOT NULL AND ttt.language_code = p_lang THEN 1.5 ELSE 1.0 END
        AS hot_score,
      ttt.language_code AS primary_language,
      c.reply_count
    FROM candidates c
    LEFT JOIN reaction_agg ra ON ra.thread_id = c.id
    LEFT JOIN content.entity_translations ttt ON ttt.entity_id = c.id AND ttt.entity_type = 'thread'::content.entity_type_enum AND ttt.is_original = true
    ORDER BY hot_score DESC
    LIMIT  LEAST(p_limit,  50)
    OFFSET GREATEST(p_offset, 0)
  )
SELECT v.id, s.hot_score, s.primary_language,
  v.author_profile, v.tags, v.reaction_totals, v.title, s.reply_count, v.created_at
FROM scored s
JOIN public.vw_content_threads_public v ON v.id = s.id
ORDER BY s.hot_score DESC;
$$;


ALTER FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") RETURNS TABLE("id" "uuid", "target_id" "uuid", "user_id" "uuid", "reaction" "content"."reaction_enum", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_entity    content.entity_type_enum;
  v_type      TEXT := lower(trim(p_target_type));
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN RETURN; END IF;

  IF v_type NOT IN ('thread', 'thread_reply', 'lens', 'workflow', 'battle', 'agent') THEN
    RETURN;
  END IF;

  IF v_type IN ('lens', 'workflow', 'battle', 'agent')
     AND NOT public.fn_artifact_can_view(v_type, p_target_id) THEN
    RETURN;
  END IF;

  v_entity := v_type::content.entity_type_enum;

  RETURN QUERY
    SELECT r.id, r.entity_id AS target_id, r.lenser_id AS user_id, r.reaction, r.created_at
    FROM content.reactions r
    WHERE r.entity_type = v_entity
      AND r.entity_id = p_target_id
      AND r.lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_added     BOOLEAN;
  v_counts    JSONB;
  v_entity    content.entity_type_enum;
  v_type      TEXT := lower(trim(p_target_type));
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = '42501';
  END IF;

  IF v_type NOT IN ('thread', 'thread_reply', 'lens', 'workflow', 'battle', 'agent') THEN
    RAISE EXCEPTION 'Invalid target_type: %', p_target_type USING ERRCODE = '22023';
  END IF;

  IF v_type IN ('lens', 'workflow', 'battle', 'agent')
     AND NOT public.fn_artifact_can_view(v_type, p_target_id) THEN
    RAISE EXCEPTION 'artifact_not_found_or_forbidden' USING ERRCODE = '42501';
  END IF;

  v_entity := v_type::content.entity_type_enum;

  IF p_reaction = 'copy'::content.reaction_enum THEN
    INSERT INTO content.reactions (entity_type, entity_id, lenser_id, reaction)
    VALUES (v_entity, p_target_id, v_lenser_id, p_reaction);
    v_added := TRUE;
  ELSE
    IF EXISTS (
      SELECT 1
      FROM content.reactions
      WHERE entity_type = v_entity
        AND entity_id = p_target_id
        AND lenser_id = v_lenser_id
        AND reaction = p_reaction
    ) THEN
      DELETE FROM content.reactions
      WHERE entity_type = v_entity
        AND entity_id = p_target_id
        AND lenser_id = v_lenser_id
        AND reaction = p_reaction;
      v_added := FALSE;
    ELSE
      INSERT INTO content.reactions (entity_type, entity_id, lenser_id, reaction)
      VALUES (v_entity, p_target_id, v_lenser_id, p_reaction);
      v_added := TRUE;
    END IF;
  END IF;

  SELECT COALESCE(jsonb_object_agg(reaction, cnt), '{}'::jsonb)
  INTO v_counts
  FROM (
    SELECT reaction, count(*)::int AS cnt
    FROM content.reactions
    WHERE entity_type = v_entity
      AND entity_id = p_target_id
    GROUP BY reaction
  ) s;

  RETURN jsonb_build_object('added', v_added, 'counts', v_counts);
END;
$$;


ALTER FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text" DEFAULT 'other'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content', 'auth'
    AS $$
DECLARE
  v_lenser_id  uuid;
  v_target_type "content"."entity_type_enum";
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  -- Validate target_type against the enum
  BEGIN
    v_target_type := p_target_type::"content"."entity_type_enum";
  EXCEPTION WHEN invalid_text_representation THEN
    RAISE EXCEPTION 'Invalid target_type: %', p_target_type;
  END;

  -- Validate and cast reason to enum
  IF p_reason NOT IN ('spam', 'harassment', 'misinformation', 'off_topic', 'other') THEN
    RAISE EXCEPTION 'Invalid reason: %', p_reason;
  END IF;

  -- Cooldown: reject if the same reporter already reported this target within 24 hours
  IF EXISTS (
    SELECT 1 FROM content.reports
    WHERE reporter_id = v_lenser_id
      AND target_type = v_target_type
      AND target_id   = p_target_id
      AND created_at  > NOW() - INTERVAL '24 hours'
  ) THEN
    RAISE EXCEPTION 'rate_limit_exceeded'
      USING DETAIL = 'You have already reported this content recently.',
            ERRCODE = 'P0429';
  END IF;

  INSERT INTO content.reports (reporter_id, target_type, target_id, reason)
  VALUES (v_lenser_id, v_target_type, p_target_id, p_reason::"content"."report_reason_enum")
  ON CONFLICT (reporter_id, target_type, target_id)
  DO UPDATE SET reason = EXCLUDED.reason;

  RETURN jsonb_build_object('reported', true);
END;
$$;


ALTER FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'content', 'public'
    AS $$
DECLARE
  v_id uuid;
BEGIN
  IF p_name IS NULL OR btrim(p_name) = '' THEN
    RAISE EXCEPTION 'Tag name is required';
  END IF;

  IF p_slug IS NULL OR btrim(p_slug) = '' THEN
    RAISE EXCEPTION 'Tag slug is required';
  END IF;

  INSERT INTO content.tags (slug, visibility)
  VALUES (btrim(p_slug), 'public')
  RETURNING id INTO v_id;

  INSERT INTO content.tag_translations (tag_id, language_code, name)
  VALUES (v_id, 'en', btrim(p_name));

  RETURN v_id;
EXCEPTION
  WHEN unique_violation THEN
    SELECT id INTO v_id
    FROM content.tags
    WHERE slug = p_slug;

    IF v_id IS NOT NULL THEN
      RETURN v_id;
    END IF;

    RAISE;
END;
$$;


ALTER FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") RETURNS TABLE("id" "uuid", "name" "text", "slug" "text", "visibility" "content"."tag_visibility_enum", "created_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT t.id, t.name, t.slug, t.visibility, t.created_at
  FROM content.tags t
  WHERE t.slug = p_slug
    AND t.visibility = 'public';
$$;


ALTER FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'content', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  DELETE FROM lensers.tag_follows
  WHERE lenser_id = v_lenser_id AND tag_id = p_tag_id;

  RETURN jsonb_build_object('following', false);
END;
$$;


ALTER FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_core_languages_list"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'core'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  SELECT jsonb_agg(
    jsonb_build_object(
      'code',        l.code,
      'name',        l.name,
      'native_name', l.native_name,
      'direction',   l.direction
    )
    ORDER BY l.name
  )
  INTO v_result
  FROM core.languages l
  WHERE l.is_active = true;

  RETURN COALESCE(v_result, '[]'::jsonb);
END;
$$;


ALTER FUNCTION "public"."fn_core_languages_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) RETURNS TABLE("reservation_id" "uuid", "status" "text", "committed_credits" numeric, "committed_usd" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'agents', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row     billing.cost_reservations%ROWTYPE;
  v_credits numeric;
BEGIN
  IF p_reservation_id IS NULL OR p_actual_credits IS NULL OR p_actual_credits < 0
     OR p_actual_usd IS NULL OR p_actual_usd < 0 THEN
    RAISE EXCEPTION 'fn_cost_commit: bad input'
      USING ERRCODE = '22023';
  END IF;

  SELECT cr.* INTO v_row FROM billing.cost_reservations cr
  WHERE cr.id = p_reservation_id FOR UPDATE;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_commit: reservation % not found', p_reservation_id
      USING ERRCODE = '23503';
  END IF;
  IF v_row.status = 'committed' THEN
    -- Idempotent: return current state.
    RETURN QUERY
    SELECT v_row.id, v_row.status, v_row.committed_credits, v_row.committed_usd;
    RETURN;
  END IF;
  IF v_row.status <> 'held' THEN
    RAISE EXCEPTION 'fn_cost_commit: reservation % cannot commit from status %',
      p_reservation_id, v_row.status
      USING ERRCODE = 'P0001';
  END IF;

  v_credits := CEIL(p_actual_credits);

  -- Reverse the original hold and post the actual commit (double-entry).
  INSERT INTO billing.ledger_entries (
    reservation_id, ai_lenser_id, org_id, direction, amount_credits, amount_usd
  )
  VALUES
    (v_row.id, v_row.ai_lenser_id, v_row.org_id, 'release',
       -v_row.reserved_credits, -v_row.reserved_usd),
    (v_row.id, v_row.ai_lenser_id, v_row.org_id, 'debit_commit',
       p_actual_credits, p_actual_usd);

  UPDATE billing.cost_reservations cr
     SET status            = 'committed',
         committed_credits = p_actual_credits,
         committed_usd     = p_actual_usd,
         committed_at      = now()
   WHERE cr.id = v_row.id;

  -- Settle into the legacy daily snapshot (kept as a materialized projection
  -- of the ledger so existing budget readers keep working).
  INSERT INTO agents.quota_snapshots AS qs (ai_lenser_id, period_date, credits_spent)
  VALUES (v_row.ai_lenser_id, CURRENT_DATE, v_credits)
  ON CONFLICT (ai_lenser_id, period_date) DO UPDATE
    SET credits_spent = qs.credits_spent + EXCLUDED.credits_spent,
        updated_at    = now();

  PERFORM automation.fn_emit_event(
    'billing.budget_committed',
    'billing', 'cost_reservations', v_row.id,
    jsonb_build_object(
      'ai_lenser_id',      v_row.ai_lenser_id,
      'committed_credits', p_actual_credits,
      'committed_usd',     p_actual_usd
    ),
    NULL
  );

  RETURN QUERY
  SELECT v_row.id, 'committed'::text, p_actual_credits, p_actual_usd;
END;
$$;


ALTER FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) IS 'CG1: held -> committed. Posts reversal of debit_hold + new debit_commit. Settles agents.quota_snapshots so legacy budget readers see the spend. Idempotent on already-committed reservations.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer DEFAULT 200) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_id    uuid;
  v_count integer := 0;
BEGIN
  -- Claim with SKIP LOCKED so multiple cron workers cannot collide.
  FOR v_id IN
    SELECT id
    FROM   billing.cost_reservations
    WHERE  status = 'held' AND held_until <= now()
    ORDER  BY held_until
    LIMIT  GREATEST(p_max_batch, 1)
    FOR    UPDATE SKIP LOCKED
  LOOP
    BEGIN
      PERFORM public.fn_cost_release(v_id, 'worker_timeout');
      v_count := v_count + 1;
    EXCEPTION WHEN OTHERS THEN
      -- Defensive: a failed release for one reservation should not abort the
      -- whole batch. Mark expired without a ledger reversal as a last resort.
      UPDATE billing.cost_reservations
         SET status = 'expired', released_at = now(), reason_released = 'expire_fallback:' || SQLERRM
       WHERE id = v_id AND status = 'held';
    END;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) IS 'CG1: cron-driven sweeper that releases reservations whose held_until passed. Uses FOR UPDATE SKIP LOCKED for safe concurrent workers.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) RETURNS TABLE("status" "text", "over_limit" boolean, "reserved_credits" numeric, "running_credits" numeric)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row       billing.cost_reservations%ROWTYPE;
  v_headroom  smallint;
  v_over      boolean := false;
BEGIN
  IF p_reservation_id IS NULL OR p_running_credits IS NULL OR p_running_credits < 0 THEN
    RAISE EXCEPTION 'fn_cost_meter_tick: bad input'
      USING ERRCODE = '22023';
  END IF;

  SELECT cr.* INTO v_row FROM billing.cost_reservations cr
  WHERE cr.id = p_reservation_id FOR UPDATE;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_meter_tick: reservation % not found', p_reservation_id
      USING ERRCODE = '23503';
  END IF;
  IF v_row.status <> 'held' THEN
    RAISE EXCEPTION 'fn_cost_meter_tick: reservation % not held (status=%)',
      p_reservation_id, v_row.status
      USING ERRCODE = 'P0001';
  END IF;

  SELECT rs.meter_headroom_pct INTO v_headroom
  FROM billing.runtime_settings rs WHERE rs.id = 1;
  v_over := p_running_credits > v_row.reserved_credits * (1 + v_headroom::numeric / 100);

  UPDATE billing.cost_reservations cr
     SET running_credits = GREATEST(cr.running_credits, p_running_credits)
   WHERE cr.id = p_reservation_id;

  RETURN QUERY
  SELECT v_row.status, v_over, v_row.reserved_credits, GREATEST(v_row.running_credits, p_running_credits);
END;
$$;


ALTER FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) IS 'CG1: monotonically updates running_credits and returns over_limit=true when running credits exceed reserved * (1 + headroom_pct). CG5 wires workers.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer DEFAULT 0, "p_est_max_output_tokens" integer DEFAULT 0, "p_units" numeric DEFAULT 0) RETURNS TABLE("pricing_snapshot_id" "uuid", "unit_type" "ai"."unit_type_enum", "estimated_usd" numeric, "estimated_credits" numeric, "credit_rate_usd" numeric, "taken_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'ai', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_pricing    ai.model_pricing%ROWTYPE;
  v_snap_id    uuid;
  v_usd        numeric(20,10);
  v_rate       numeric(20,10) := 0.001;       -- TODO(CG8): per-org override
  v_credits    numeric(20,6);
BEGIN
  IF p_model_id IS NULL THEN
    RAISE EXCEPTION 'fn_cost_quote: p_model_id is required'
      USING ERRCODE = '22023';
  END IF;
  IF p_est_input_tokens < 0 OR p_est_max_output_tokens < 0 OR p_units < 0 THEN
    RAISE EXCEPTION 'fn_cost_quote: negative usage not allowed'
      USING ERRCODE = '22023';
  END IF;

  SELECT *
  INTO   v_pricing
  FROM   ai.model_pricing
  WHERE  model_id = p_model_id
    AND  effective_from <= now()
    AND  (effective_to IS NULL OR effective_to > now())
  ORDER  BY effective_from DESC
  LIMIT  1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'E_PRICING_UNAVAILABLE: no active price row for model %', p_model_id
      USING ERRCODE = 'P0001';
  END IF;

  IF v_pricing.unit_type = 'tokens' THEN
    v_usd :=
        (p_est_input_tokens::numeric    / 1000.0) * v_pricing.input_cost_per_1k_tokens
      + (p_est_max_output_tokens::numeric / 1000.0) * v_pricing.output_cost_per_1k_tokens;
  ELSE
    v_usd := p_units * COALESCE(v_pricing.cost_per_unit, 0);
  END IF;

  -- Reuse a recent snapshot for the same source pricing row when present
  -- (under 5 min old) to avoid table bloat on hot paths. Alias the table so
  -- the OUT-column `taken_at` does not collide with the table column.
  SELECT ps.id
  INTO   v_snap_id
  FROM   billing.pricing_snapshots ps
  WHERE  ps.source_pricing_id = v_pricing.id
    AND  ps.taken_at > now() - interval '5 minutes'
  ORDER  BY ps.taken_at DESC
  LIMIT  1;

  IF v_snap_id IS NULL THEN
    INSERT INTO billing.pricing_snapshots (
      model_id, source_pricing_id, unit_type,
      input_cpm_usd, output_cpm_usd, unit_cost_usd, credit_rate_usd
    )
    VALUES (
      p_model_id, v_pricing.id, v_pricing.unit_type,
      v_pricing.input_cost_per_1k_tokens,
      v_pricing.output_cost_per_1k_tokens,
      v_pricing.cost_per_unit,
      v_rate
    )
    RETURNING id INTO v_snap_id;
  END IF;

  v_credits := CEIL(v_usd / v_rate);

  RETURN QUERY
  SELECT v_snap_id, v_pricing.unit_type, v_usd::numeric, v_credits, v_rate::numeric, now();
END;
$$;


ALTER FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) IS 'CG1: returns an estimated dollar/credit cost AND pins a pricing_snapshot row that fn_cost_reserve will reference. Safe for authenticated callers (read-only pricing; no side effects beyond the immutable snapshot insert).';



CREATE OR REPLACE FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") RETURNS TABLE("reservation_id" "uuid", "status" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_row billing.cost_reservations%ROWTYPE;
BEGIN
  IF p_reservation_id IS NULL THEN
    RAISE EXCEPTION 'fn_cost_release: reservation_id required'
      USING ERRCODE = '22023';
  END IF;

  SELECT cr.* INTO v_row FROM billing.cost_reservations cr
  WHERE cr.id = p_reservation_id FOR UPDATE;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_release: reservation % not found', p_reservation_id
      USING ERRCODE = '23503';
  END IF;
  IF v_row.status IN ('released','expired') THEN
    RETURN QUERY SELECT v_row.id, v_row.status;
    RETURN;
  END IF;
  IF v_row.status <> 'held' THEN
    RAISE EXCEPTION 'fn_cost_release: reservation % cannot release from status %',
      p_reservation_id, v_row.status
      USING ERRCODE = 'P0001';
  END IF;

  INSERT INTO billing.ledger_entries (
    reservation_id, ai_lenser_id, org_id, direction, amount_credits, amount_usd
  )
  VALUES (
    v_row.id, v_row.ai_lenser_id, v_row.org_id, 'release',
    -v_row.reserved_credits, -v_row.reserved_usd
  );

  UPDATE billing.cost_reservations cr
     SET status           = 'released',
         reason_released  = p_reason,
         released_at      = now()
   WHERE cr.id = v_row.id;

  PERFORM automation.fn_emit_event(
    'billing.budget_released',
    'billing', 'cost_reservations', v_row.id,
    jsonb_build_object(
      'ai_lenser_id', v_row.ai_lenser_id,
      'reason',       COALESCE(p_reason, ''),
      'amount_credits', v_row.reserved_credits
    ),
    NULL
  );

  RETURN QUERY SELECT v_row.id, 'released'::text;
END;
$$;


ALTER FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") IS 'CG1: held -> released. Posts a release ledger entry reversing the hold. Idempotent on already-released/expired reservations.';



CREATE OR REPLACE FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb" DEFAULT '{}'::"jsonb", "p_actor_id" "uuid" DEFAULT NULL::"uuid", "p_org_id" "uuid" DEFAULT NULL::"uuid", "p_ttl" interval DEFAULT NULL::interval) RETURNS TABLE("reservation_id" "uuid", "status" "text", "held_until" timestamp with time zone, "shadow_mode" boolean)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'agents', 'public', 'pg_catalog'
    AS $$
DECLARE
  v_settings       billing.runtime_settings%ROWTYPE;
  v_snap           billing.pricing_snapshots%ROWTYPE;
  v_existing       billing.cost_reservations%ROWTYPE;
  v_lenser         agents.ai_lensers%ROWTYPE;
  v_policy_limit   numeric;
  v_already_held   numeric;
  v_already_today  numeric;
  v_kill_switch    boolean;
  v_paused         boolean;
  v_budget_enforce boolean := false;
  v_id             uuid;
  v_ttl            interval;
BEGIN
  IF p_ai_lenser_id IS NULL OR p_pricing_snapshot_id IS NULL
     OR p_reserved_credits IS NULL OR p_provider_key IS NULL
     OR p_idempotency_key IS NULL OR LENGTH(p_idempotency_key) = 0 THEN
    RAISE EXCEPTION 'fn_cost_reserve: missing required parameter'
      USING ERRCODE = '22023';
  END IF;
  IF p_reserved_credits <= 0 THEN
    RAISE EXCEPTION 'fn_cost_reserve: reserved_credits must be > 0'
      USING ERRCODE = '22023';
  END IF;
  IF p_reserved_usd IS NULL OR p_reserved_usd < 0 THEN
    RAISE EXCEPTION 'fn_cost_reserve: reserved_usd must be >= 0'
      USING ERRCODE = '22023';
  END IF;

  SELECT * INTO v_settings FROM billing.runtime_settings WHERE id = 1;
  v_ttl := COALESCE(p_ttl, v_settings.reservation_default_ttl);

  -- Idempotency short-circuit — replay returns the original row unchanged.
  SELECT cr.* INTO v_existing
  FROM   billing.cost_reservations cr
  WHERE  cr.ai_lenser_id = p_ai_lenser_id
    AND  cr.idempotency_key = p_idempotency_key;
  IF FOUND THEN
    RETURN QUERY
    SELECT v_existing.id, v_existing.status, v_existing.held_until, v_existing.shadow_mode;
    RETURN;
  END IF;

  SELECT ps.* INTO v_snap FROM billing.pricing_snapshots ps WHERE ps.id = p_pricing_snapshot_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_reserve: pricing_snapshot % not found', p_pricing_snapshot_id
      USING ERRCODE = '23503';
  END IF;

  SELECT al.* INTO v_lenser FROM agents.ai_lensers al WHERE al.id = p_ai_lenser_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'fn_cost_reserve: ai_lenser % not found', p_ai_lenser_id
      USING ERRCODE = '23503';
  END IF;
  IF v_lenser.is_active = false OR v_lenser.suspended_at IS NOT NULL THEN
    RAISE EXCEPTION 'E_AGENT_PAUSED: ai_lenser % is not active', p_ai_lenser_id
      USING ERRCODE = 'P0001';
  END IF;

  -- Workspace kill switch + budget enforcement flag (both on workspace_settings).
  SELECT COALESCE(ws.global_kill_switch, false),
         COALESCE(ws.agent_paused,       false),
         COALESCE(ws.budget_enforce,     false)
  INTO   v_kill_switch, v_paused, v_budget_enforce
  FROM   agents.workspace_settings ws
  WHERE  ws.ai_lenser_id = p_ai_lenser_id;
  IF v_kill_switch OR v_paused THEN
    RAISE EXCEPTION 'E_KILL_SWITCH: workspace or agent is paused'
      USING ERRCODE = 'P0001';
  END IF;

  -- Legacy daily-credit limit (agents.policies.spending_limit_credits).
  -- CG2 supersedes by consulting billing.budget_policies.
  IF v_budget_enforce THEN
    SELECT pol.spending_limit_credits
    INTO   v_policy_limit
    FROM   agents.policies pol
    WHERE  pol.ai_lenser_id = p_ai_lenser_id;
  END IF;

  IF v_policy_limit IS NOT NULL AND v_settings.enforce THEN
    SELECT COALESCE(SUM(cr.reserved_credits), 0)
    INTO   v_already_held
    FROM   billing.cost_reservations cr
    WHERE  cr.ai_lenser_id = p_ai_lenser_id
      AND  cr.status = 'held'
      AND  cr.held_until > now();

    SELECT COALESCE(qs.credits_spent, 0)
    INTO   v_already_today
    FROM   agents.quota_snapshots qs
    WHERE  qs.ai_lenser_id = p_ai_lenser_id
      AND  qs.period_date  = CURRENT_DATE;

    IF (COALESCE(v_already_held, 0) + COALESCE(v_already_today, 0) + p_reserved_credits) > v_policy_limit THEN
      RAISE EXCEPTION
        'E_BUDGET_EXCEEDED: ai_lenser % would breach daily limit % (held=% today=% requested=%)',
        p_ai_lenser_id, v_policy_limit,
        v_already_held, v_already_today, p_reserved_credits
        USING ERRCODE = 'P0001';
    END IF;
  END IF;

  -- Persist reservation + paired debit_hold ledger entry atomically.
  INSERT INTO billing.cost_reservations (
    ai_lenser_id, actor_id, org_id,
    model_id, provider_key, pricing_snapshot_id,
    reserved_credits, reserved_usd,
    status, context, idempotency_key, shadow_mode,
    held_until
  )
  VALUES (
    p_ai_lenser_id, p_actor_id, p_org_id,
    v_snap.model_id, p_provider_key, p_pricing_snapshot_id,
    p_reserved_credits, p_reserved_usd,
    'held', COALESCE(p_context, '{}'::jsonb), p_idempotency_key,
    NOT v_settings.enforce,
    now() + v_ttl
  )
  RETURNING id INTO v_id;

  INSERT INTO billing.ledger_entries (
    reservation_id, ai_lenser_id, org_id, direction,
    amount_credits, amount_usd
  )
  VALUES (
    v_id, p_ai_lenser_id, p_org_id, 'debit_hold',
    p_reserved_credits, p_reserved_usd
  );

  PERFORM automation.fn_emit_event(
    'billing.budget_reserved',
    'billing', 'cost_reservations', v_id,
    jsonb_build_object(
      'ai_lenser_id',     p_ai_lenser_id,
      'reserved_credits', p_reserved_credits,
      'reserved_usd',     p_reserved_usd,
      'provider_key',     p_provider_key,
      'shadow_mode',      NOT v_settings.enforce
    ),
    NULL
  );

  RETURN QUERY
  SELECT v_id, 'held'::text, now() + v_ttl, NOT v_settings.enforce;
END;
$$;


ALTER FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) IS 'CG1: creates a held reservation + debit_hold ledger entry. Service_role only. Idempotent on (ai_lenser_id, idempotency_key). Fails closed on budget when billing.runtime_settings.enforce = true.';



CREATE OR REPLACE FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text" DEFAULT NULL::"text", "p_initial_members" "jsonb" DEFAULT '[]'::"jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
  v_team_id   uuid;
  v_team      jsonb;
  v_member    jsonb;
  v_members   jsonb := COALESCE(p_initial_members, '[]'::jsonb);
BEGIN
  IF jsonb_typeof(v_members) <> 'array' THEN
    RAISE EXCEPTION 'p_initial_members must be a JSON array' USING ERRCODE = '22023';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM agents.ownerships o
    WHERE o.ai_lenser_id = p_ai_lenser_id
      AND o.owner_lenser_id = v_lenser_id
      AND o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'create_team_forbidden' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.teams (ai_lenser_id, name, description, status, is_active)
  VALUES (p_ai_lenser_id, p_name, p_description, 'active', true)
  RETURNING id, to_jsonb(agents.teams.*) INTO v_team_id, v_team;

  FOR v_member IN SELECT * FROM jsonb_array_elements(v_members) LOOP
    INSERT INTO agents.team_members (
      team_id, agent_id, role, responsibility, lane, sort_order, is_active
    ) VALUES (
      v_team_id,
      (v_member->>'agent_id')::uuid,
      v_member->>'role',
      v_member->>'responsibility',
      v_member->>'lane',
      COALESCE((v_member->>'sort_order')::integer, 0),
      COALESCE((v_member->>'is_active')::boolean, true)
    );
  END LOOP;

  RETURN v_team;
END;
$$;


ALTER FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'auth'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  v_result := agents.fn_create_ai_lenser(
    p_owner_lenser_id := p_owner_lenser_id,
    p_handle          := p_handle,
    p_display_name    := p_display_name,
    p_ai_model_id     := p_ai_model_id
  );
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") IS 'Public wrapper for agents.fn_create_ai_lenser. Auth enforced by inner function. Fixes PGRST202 404 in agentsRepository.createAgent.';



CREATE OR REPLACE FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_catalog'
    AS $$
BEGIN
  RETURN public.fn_insert_notification(
    p_lenser_id,
    'automation_action',
    p_title,
    p_body,
    NULL,
    COALESCE(p_payload, '{}'::jsonb)
  );
END;
$$;


ALTER FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") IS 'Phase U2: thin wrapper around public.fn_insert_notification used by automation.fn_dispatch_action when action_kind=notify. Type is fixed to "automation_action" so users can filter automation-driven entries from human-driven ones in their feed.';



CREATE OR REPLACE FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text" DEFAULT 'open'::"text", "p_handicap_config" "jsonb" DEFAULT '{}'::"jsonb", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_lens_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_slug      text;
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  v_slug := lower(regexp_replace(p_title, '[^a-z0-9\s-]', '', 'gi'));
  v_slug := regexp_replace(v_slug, '\s+', '-', 'g');
  v_slug := regexp_replace(v_slug, '-+', '-', 'g');
  v_slug := left(v_slug, 80) || '-' || substr(md5(random()::text), 1, 6);

  RETURN QUERY
  INSERT INTO battles.battles (
    title, task_prompt, battle_type, voter_eligibility,
    handicap_config, creator_lenser_id, slug, status,
    workflow_id, lens_id
  ) VALUES (
    p_title, p_task_prompt, p_battle_type, p_voter_eligibility,
    COALESCE(p_handicap_config, '{}'::jsonb), v_lenser_id, v_slug, 'draft',
    p_workflow_id, p_lens_id
  )
  RETURNING
    id, slug, title, task_prompt, status, total_vote_count, published_at,
    voting_opens_at, voting_closes_at, battle_type, voter_eligibility, handicap_config,
    creator_lenser_id, forum_thread_id, workflow_id, lens_id,
    execution_starts_at, auto_publish, voting_duration_hours, vote_velocity, og_image_url,
    winner_contender_id, parent_battle_id, deleted_at;
END;
$$;


ALTER FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text" DEFAULT 'lifecycle'::"text") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'extensions', 'public'
    AS $$
DECLARE
  v_actor UUID := lensers.get_auth_lenser_id();
  v_rules_json JSONB;
  v_hash TEXT;
BEGIN
  SELECT jsonb_build_object(
    'snapshot_reason', p_reason,
    'title', b.title,
    'slug', b.slug,
    'task_prompt', b.task_prompt,
    'rubric_id', b.rubric_id,
    'status', b.status,
    'battle_type', b.battle_type,
    'contender_structure', b.contender_structure,
    'judging_mode', b.judging_mode,
    'task_source', b.task_source,
    'challenge_type', b.challenge_type,
    'generated_challenge_id', b.generated_challenge_id,
    'voter_eligibility', b.voter_eligibility,
    'max_contenders', b.max_contenders,
    'handicap_config', b.handicap_config,
    'workflow_id', b.workflow_id,
    'lens_id', b.lens_id,
    'content_type', b.content_type,
    'execution_starts_at', b.execution_starts_at,
    'auto_publish', b.auto_publish,
    'voting_duration_hours', b.voting_duration_hours,
    'ai_judge_enabled', b.ai_judge_enabled,
    'ai_judge_model_key', b.ai_judge_model_key,
    'ai_judge_prompt_hash', CASE
      WHEN b.ai_judge_prompt IS NULL THEN NULL
      ELSE encode(extensions.digest(b.ai_judge_prompt, 'sha256'), 'hex')
    END,
    'published_at', b.published_at,
    'captured_at', now()
  )
  INTO v_rules_json
  FROM battles.battles b
  WHERE b.id = p_battle_id;

  IF v_rules_json IS NULL THEN
    RAISE EXCEPTION 'battle_not_found: %', p_battle_id USING ERRCODE = 'P0002';
  END IF;

  v_hash := encode(extensions.digest(v_rules_json::text, 'sha256'), 'hex');

  INSERT INTO battles.rule_snapshots (battle_id, snapshot_hash, rules_json, created_by)
  VALUES (p_battle_id, v_hash, v_rules_json, v_actor);

  PERFORM public.fn_log_artifact_lifecycle_event(
    'battle',
    p_battle_id,
    'config_snapshot_create',
    v_actor,
    jsonb_build_object('snapshot_hash', v_hash, 'reason', p_reason)
  );

  RETURN v_hash;
END;
$$;


ALTER FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer DEFAULT 3) RETURNS "battles"."series"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid       UUID := auth.uid();
  v_template  battles.templates%ROWTYPE;
  v_series    battles.series%ROWTYPE;
  v_battle_id UUID;
  v_slug      TEXT;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_round_count IS NULL OR p_round_count < 1 OR p_round_count > 16 THEN
    RAISE EXCEPTION 'invalid_round_count' USING ERRCODE = '22023';
  END IF;

  SELECT * INTO v_template
    FROM battles.templates
   WHERE id = p_template_id
     AND deleted_at IS NULL
     AND (is_public = true OR creator_lenser_id = v_uid);
  IF v_template IS NULL THEN
    RAISE EXCEPTION 'template_not_accessible' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.series (title, template_id, creator_lenser_id, round_count)
  VALUES (p_title, p_template_id, v_uid, p_round_count)
  RETURNING * INTO v_series;

  v_slug := lower(regexp_replace(p_title, '[^a-zA-Z0-9]+', '-', 'g'))
            || '-r1-' || substr(v_series.id::text, 1, 8);

  v_battle_id := public.fn_battles_create_from_template(
    p_template_id, p_title || ' — Round 1', v_slug
  );

  INSERT INTO battles.series_rounds (series_id, round_number, battle_id)
  VALUES (v_series.id, 1, v_battle_id);

  RETURN v_series;
END $$;


ALTER FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_next_version integer;
  v_result       jsonb;
BEGIN
  -- Mark existing rubrics as not current
  UPDATE agents.evaluation_rubrics
  SET is_current = false
  WHERE evaluation_id = p_evaluation_id;

  -- Get next version
  SELECT COALESCE(MAX(version), 0) + 1 INTO v_next_version
  FROM agents.evaluation_rubrics
  WHERE evaluation_id = p_evaluation_id;

  INSERT INTO agents.evaluation_rubrics (evaluation_id, version, criteria, is_current)
  VALUES (p_evaluation_id, v_next_version, p_criteria, true)
  RETURNING to_jsonb(agents.evaluation_rubrics.*) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text" DEFAULT NULL::"text", "p_scoring_rules" "jsonb" DEFAULT '{}'::"jsonb", "p_dataset_uri" "text" DEFAULT NULL::"text", "p_cases" "jsonb" DEFAULT '[]'::"jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_eval_id uuid;
  v_case    jsonb;
  v_result  jsonb;
BEGIN
  INSERT INTO agents.evaluations
    (owner_lenser_id, ai_lenser_id, target_type, target_id, name, description, scoring_rules, dataset_uri)
  VALUES
    (p_owner_lenser_id, p_ai_lenser_id, p_target_type, p_target_id, p_name, p_description,
     COALESCE(p_scoring_rules, '{}'), p_dataset_uri)
  RETURNING id, to_jsonb(agents.evaluations.*) INTO v_eval_id, v_result;

  IF jsonb_array_length(COALESCE(p_cases, '[]'::jsonb)) > 0 THEN
    FOR v_case IN SELECT * FROM jsonb_array_elements(p_cases) LOOP
      INSERT INTO agents.evaluation_cases
        (evaluation_id, input, expected, weight, tags)
      VALUES (
        v_eval_id,
        v_case->'input',
        v_case->'expected',
        COALESCE((v_case->>'weight')::integer, 1),
        COALESCE(ARRAY(SELECT jsonb_array_elements_text(v_case->'tags')), '{}')
      );
    END LOOP;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_language_code" "text" DEFAULT 'en'::"text", "p_params" "jsonb" DEFAULT '[]'::"jsonb", "p_tag_ids" "uuid"[] DEFAULT '{}'::"uuid"[], "p_parent_lens_id" "uuid" DEFAULT NULL::"uuid", "p_forked_from_execution_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_id uuid;
BEGIN
  v_id := lenses.fn_create_lens(
    p_visibility               := p_visibility::content.visibility_enum,
    p_template_body            := p_template_body,
    p_title                    := p_title,
    p_description              := p_description,
    p_language_code            := p_language_code,
    p_params                   := p_params,
    p_tag_ids                  := p_tag_ids,
    p_parent_lens_id           := p_parent_lens_id,
    p_forked_from_execution_id := p_forked_from_execution_id
  );
  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") IS 'Public wrapper for lenses.fn_create_lens. Auth enforced by inner function. Fixes PGRST202 404 in lensesRepository.createLens.';



CREATE OR REPLACE FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text" DEFAULT NULL::"text", "p_external_url" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "owner_lenser_id" "uuid", "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "content_text" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'media', 'lensers'
    AS $$
  INSERT INTO media.objects
    (workspace_id, owner_lenser_id, media_type, mime_type, name,
     content_text, external_url, lifecycle_state)
  VALUES (
    p_workspace_id, lensers.get_auth_lenser_id(), p_media_type, p_mime_type, p_name,
    p_content_text, p_external_url,
    CASE WHEN p_content_text IS NOT NULL OR p_external_url IS NOT NULL
         THEN 'active' ELSE 'pending' END
  )
  RETURNING
    id, workspace_id, owner_lenser_id, bucket, object_key, external_url,
    mime_type, media_type, name, content_text, byte_size, visibility,
    lifecycle_state, metadata, created_at, updated_at;
$$;


ALTER FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") IS 'Security wrapper: insert a media.objects row for the current user. lifecycle_state is auto-set to "active" when content or URL is provided, else "pending".';



CREATE OR REPLACE FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run               agents.team_runs;
  v_total_steps       integer;
  v_total_tools       integer;
  v_total_cost        numeric;
  v_total_mem_writes  integer;
  v_eval_score        numeric;
  v_outcome           text;
  v_title             text;
  v_metrics           jsonb;
  v_step_statuses     jsonb;
  v_tool_statuses     jsonb;
  v_duration_secs     numeric;
  v_report_id         uuid;
BEGIN
  SELECT * INTO v_run
    FROM agents.team_runs
   WHERE id = p_team_run_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'team run not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_run.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF v_run.status NOT IN ('completed','failed','cancelled') THEN
    RAISE EXCEPTION 'run must be terminal before creating a report' USING ERRCODE = '22023';
  END IF;

  IF EXISTS (
    SELECT 1 FROM agents.run_reports WHERE team_run_id = p_team_run_id
  ) THEN
    RAISE EXCEPTION 'report already exists for this run' USING ERRCODE = '23505';
  END IF;

  -- Aggregate step count
  SELECT COUNT(*)
    INTO v_total_steps
    FROM agents.agent_run_steps
   WHERE team_run_id = p_team_run_id;

  -- Aggregate tool count and cost
  SELECT
    COUNT(*),
    COALESCE(SUM(cost_estimate), 0)
  INTO v_total_tools, v_total_cost
    FROM agents.tool_invocations
   WHERE team_run_id = p_team_run_id;

  -- Aggregate memory writes
  SELECT COUNT(*)
    INTO v_total_mem_writes
    FROM agents.memories
   WHERE team_run_id = p_team_run_id
     AND source IN ('agent','tool');

  -- Latest completed evaluation score for the workflow
  SELECT er.score
    INTO v_eval_score
    FROM agents.evaluation_runs er
    JOIN agents.evaluations e ON e.id = er.evaluation_id
   WHERE e.target_type = 'workflow'
     AND e.target_id   = v_run.workflow_id
     AND er.status     = 'completed'
   ORDER BY er.completed_at DESC NULLS LAST
   LIMIT 1;

  -- Detect 'killed': cancelled + kill_switch policy denial
  IF EXISTS (
    SELECT 1
      FROM agents.policy_evaluations
     WHERE team_run_id = p_team_run_id
       AND policy_type = 'kill_switch'
       AND verdict     = 'deny'
  ) AND v_run.status = 'cancelled' THEN
    v_outcome := 'killed';
  ELSE
    CASE v_run.status
      WHEN 'completed' THEN v_outcome := 'success';
      WHEN 'failed'    THEN v_outcome := 'failed';
      WHEN 'cancelled' THEN v_outcome := 'cancelled';
      ELSE                  v_outcome := 'partial';
    END CASE;
  END IF;

  -- Title
  SELECT 'Run Report: ' || COALESCE(w.title, p_team_run_id::text)
    INTO v_title
    FROM lenses.workflows w
   WHERE w.id = v_run.workflow_id;

  IF v_title IS NULL THEN
    v_title := 'Run Report: ' || p_team_run_id::text;
  END IF;

  -- Step status breakdown
  SELECT COALESCE(jsonb_object_agg(status, cnt), '{}'::jsonb)
    INTO v_step_statuses
    FROM (
      SELECT status, COUNT(*) AS cnt
        FROM agents.agent_run_steps
       WHERE team_run_id = p_team_run_id
       GROUP BY status
    ) s;

  -- Tool status breakdown
  SELECT COALESCE(jsonb_object_agg(status, cnt), '{}'::jsonb)
    INTO v_tool_statuses
    FROM (
      SELECT status, COUNT(*) AS cnt
        FROM agents.tool_invocations
       WHERE team_run_id = p_team_run_id
       GROUP BY status
    ) t;

  -- Duration
  IF v_run.started_at IS NOT NULL AND v_run.completed_at IS NOT NULL THEN
    v_duration_secs := EXTRACT(EPOCH FROM (v_run.completed_at - v_run.started_at));
  ELSE
    v_duration_secs := NULL;
  END IF;

  -- Compose metrics
  v_metrics := jsonb_build_object(
    'step_statuses',  v_step_statuses,
    'tool_statuses',  v_tool_statuses,
    'duration_seconds', v_duration_secs
  );

  INSERT INTO agents.run_reports (
    ai_lenser_id,
    team_run_id,
    workflow_run_id,
    title,
    metrics,
    total_steps,
    total_tool_invocations,
    total_memory_writes,
    total_cost_estimate,
    evaluation_score,
    outcome
  ) VALUES (
    v_run.ai_lenser_id,
    p_team_run_id,
    v_run.workflow_run_id,
    v_title,
    v_metrics,
    COALESCE(v_total_steps, 0),
    COALESCE(v_total_tools, 0),
    COALESCE(v_total_mem_writes, 0),
    COALESCE(v_total_cost, 0),
    v_eval_score,
    v_outcome
  )
  RETURNING id INTO v_report_id;

  RETURN v_report_id;
END;
$$;


ALTER FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[] DEFAULT ARRAY[]::"date"[], "p_timezone" "text" DEFAULT 'UTC'::"text") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  INSERT INTO lenses.schedule_calendars
    (lenser_id, name, kind, dates, timezone, is_seed)
  VALUES
    (lensers.get_auth_lenser_id(), p_name, p_kind, p_dates, p_timezone, false)
  RETURNING id;
$$;


ALTER FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") IS 'Security wrapper: create a schedule calendar owned by the current user. lenser_id is derived from the session — callers cannot supply it.';



CREATE OR REPLACE FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid" DEFAULT NULL::"uuid", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "agents"."scratchpad_runs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_actor uuid := auth.uid();
  v_row   agents.scratchpad_runs;
BEGIN
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'authentication required' USING ERRCODE = '42501';
  END IF;
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.scratchpad_runs (ai_lenser_id, actor_lenser_id, prompt, model_id, metadata)
  VALUES (p_ai_lenser_id, v_actor, p_prompt, p_model_id, COALESCE(p_metadata, '{}'::jsonb))
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text" DEFAULT 'en'::"text") RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "visibility" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
DECLARE
  v_tag_id uuid;
BEGIN
  IF btrim(p_name) = '' OR p_name IS NULL THEN
    RAISE EXCEPTION 'Tag name is required';
  END IF;
  IF btrim(p_slug) = '' OR p_slug IS NULL THEN
    RAISE EXCEPTION 'Tag slug is required';
  END IF;

  -- Idempotent insert of base tag
  INSERT INTO content.tags (slug, visibility)
  VALUES (btrim(p_slug), 'public')
  ON CONFLICT ON CONSTRAINT tags_slug_key DO NOTHING;

  -- Resolve tag id (handles both new insert and pre-existing slug)
  SELECT t.id INTO v_tag_id
  FROM content.tags t
  WHERE t.slug = btrim(p_slug);

  IF v_tag_id IS NULL THEN
    RAISE EXCEPTION 'Failed to resolve tag for slug: %', p_slug;
  END IF;

  -- Idempotent insert of translation for the requested language
  INSERT INTO content.tag_translations (tag_id, language_code, name)
  VALUES (v_tag_id, btrim(p_language_code), btrim(p_name))
  ON CONFLICT ON CONSTRAINT tag_translations_tag_id_language_id_key DO NOTHING;

  -- Return resolved row from the public view
  RETURN QUERY
    SELECT v.id, v.slug, v.name, v.visibility
    FROM public.vw_tags_public_stats v
    WHERE v.id = v_tag_id;
END;
$$;


ALTER FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_run_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_assignment_id" "uuid" DEFAULT NULL::"uuid", "p_team_id" "uuid" DEFAULT NULL::"uuid", "p_approval_status" "text" DEFAULT 'pending'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  INSERT INTO agents.team_runs
    (ai_lenser_id, workflow_id, workflow_run_id, workflow_assignment_id,
     team_id, status, approval_status, started_at)
  VALUES
    (p_ai_lenser_id, p_workflow_id, p_workflow_run_id, p_workflow_assignment_id,
     p_team_id, 'running', p_approval_status, now())
  RETURNING to_jsonb(agents.team_runs.*) INTO v_result;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  INSERT INTO content.thread_replies (thread_id, lenser_id, content, parent_reply_id)
  VALUES (p_thread_id, lensers.get_auth_lenser_id(), p_content, p_parent_reply_id)
  RETURNING id, lenser_id;
$$;


ALTER FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") IS 'Security wrapper: insert a reply into a thread as the current user. Returns the new reply id and lenser_id.';



CREATE OR REPLACE FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text" DEFAULT 'single_elimination'::"text", "p_max_contenders" integer DEFAULT 8, "p_battle_type" "text" DEFAULT 'ai_vs_ai'::"text", "p_ai_judge_enabled" boolean DEFAULT false) RETURNS "battles"."tournaments"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_tournament battles.tournaments;
  v_slug       TEXT;
BEGIN
  v_slug := lower(regexp_replace(p_title, '[^a-zA-Z0-9]', '-', 'g')) || '-' || to_char(now(), 'YYYYMMDD-HH24MI');

  INSERT INTO battles.tournaments
    (title, slug, creator_lenser_id, format, status, max_contenders, battle_type, ai_judge_enabled)
  VALUES
    (p_title, v_slug, auth.uid(), p_format, 'registration', p_max_contenders, p_battle_type, p_ai_judge_enabled)
  RETURNING * INTO v_tournament;

  RETURN v_tournament;
END;
$$;


ALTER FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_visibility" "text" DEFAULT 'public'::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthorized' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO lenses.workflows (lenser_id, title, description, visibility)
  VALUES (v_lenser_id, p_title, p_description, p_visibility)
  RETURNING
    lenses.workflows.id,
    lenses.workflows.lenser_id,
    lenses.workflows.title,
    lenses.workflows.description,
    lenses.workflows.visibility,
    lenses.workflows.battle_count,
    lenses.workflows.created_at,
    lenses.workflows.updated_at;
END;
$$;


ALTER FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") IS 'Creates a workflow. Lenser identity is resolved from JWT via get_auth_lenser_id() — p_lenser_id was removed to prevent identity spoofing.';



CREATE OR REPLACE FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN lenses.fn_create_workflow_version(p_workflow_id, p_changelog);
END;
$$;


ALTER FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") IS 'Public wrapper: snapshots the current workflow DAG into a new draft version. Owner-only. Delegates ownership/auth check to lenses.fn_create_workflow_version.';



CREATE OR REPLACE FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_result    jsonb;
  v_data      jsonb := COALESCE(p_data, '{}'::jsonb);
  v_cols      text;
  v_selects   text;
BEGIN
  IF p_table_name NOT IN (
    'personality_profiles', 'memory_profiles', 'tool_profiles', 'model_profiles',
    'workflow_assignments', 'evaluation_cases', 'evaluation_rubrics',
    'evaluation_baselines', 'tools_registry', 'evaluations',
    'team_members', 'team_edges', 'agent_run_events', 'agent_run_steps'
  ) THEN
    RAISE EXCEPTION 'create_forbidden: table % not in allowlist', p_table_name
      USING ERRCODE = '42501';
  END IF;

  -- Drop fields whose column defaults should fire when the caller omits a
  -- value. Today these are id, created_at, updated_at. Stripping them keeps
  -- the INSERT column list lean so DEFAULT clauses apply.
  v_data := v_data - 'id' - 'created_at' - 'updated_at';

  IF jsonb_typeof(v_data) <> 'object' THEN
    RAISE EXCEPTION 'create_forbidden: p_data must be a JSON object' USING ERRCODE = '22023';
  END IF;

  -- Empty payloads: rely entirely on column defaults / NOT NULL violations
  -- (the caller did not specify any fields).
  IF (SELECT count(*) FROM jsonb_object_keys(v_data)) = 0 THEN
    EXECUTE format(
      'INSERT INTO agents.%I DEFAULT VALUES RETURNING to_jsonb(%I.*)',
      p_table_name, p_table_name
    ) INTO v_result;
    RETURN v_result;
  END IF;

  -- Build the column list and the matching SELECT-from-record list from the
  -- keys actually present in v_data. jsonb_populate_record casts each value
  -- to the column's true type; we then project only those columns so the
  -- omitted ones fall back to their DEFAULT clause on INSERT.
  SELECT
    string_agg(quote_ident(k), ', '            ORDER BY k),
    string_agg(format('r.%I', k), ', '         ORDER BY k)
  INTO v_cols, v_selects
  FROM jsonb_object_keys(v_data) AS k;

  EXECUTE format(
    'INSERT INTO agents.%I (%s) '
    'SELECT %s FROM jsonb_populate_record(NULL::agents.%I, %L::jsonb) AS r '
    'RETURNING to_jsonb(%I.*)',
    p_table_name,
    v_cols,
    v_selects,
    p_table_name,
    v_data::text,
    p_table_name
  ) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") IS 'Create an agents workspace record. Strips id/created_at/updated_at from the JSONB payload so column defaults (gen_random_uuid, now) fire. Inserts only the columns the caller actually provided. Whitelisted on p_table_name.';



CREATE OR REPLACE FUNCTION "public"."fn_deactivate_account"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_profile RECORD;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT id, user_id, status
  INTO v_profile
  FROM lensers.profiles
  WHERE user_id = v_uid
  FOR UPDATE;

  IF NOT FOUND OR v_profile.status <> 'active' THEN
    RETURN jsonb_build_object('success', false, 'reason', 'not_active');
  END IF;

  UPDATE lensers.profiles
  SET status = 'deactivated',
      updated_at = now()
  WHERE id = v_profile.id;

  PERFORM lensers.log_account_lifecycle_event(
    v_profile.id,
    v_profile.user_id,
    'account_deactivated',
    v_profile.status,
    'deactivated'::lensers.lenser_status,
    'user_action',
    jsonb_build_object('reason', 'user_requested_deactivation')
  );

  RETURN jsonb_build_object('success', true, 'status', 'deactivated');
END;
$$;


ALTER FUNCTION "public"."fn_deactivate_account"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text" DEFAULT NULL::"text", "p_modifications" "jsonb" DEFAULT NULL::"jsonb") RETURNS TABLE("request_id" "uuid", "ai_lenser_id" "uuid", "team_id" "uuid", "workflow_id" "uuid", "workflow_run_id" "uuid", "workflow_assignment_id" "uuid", "approval_status" "text", "run_status" "text", "metadata" "jsonb", "decided_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lenses', 'lensers'
    AS $$
DECLARE
  v_actor          uuid;
  v_existing       agents.team_runs%ROWTYPE;
  v_decision_at    timestamptz := now();
  v_event_type     text;
  v_new_metadata   jsonb;
  v_terminal_run   text;
BEGIN
  IF p_decision NOT IN ('approved', 'rejected', 'modified') THEN
    RAISE EXCEPTION 'Invalid decision %. Allowed: approved | rejected | modified.', p_decision
      USING ERRCODE = '22023';
  END IF;

  IF p_decision = 'modified' AND (p_modifications IS NULL OR jsonb_typeof(p_modifications) <> 'object') THEN
    RAISE EXCEPTION 'modify-and-approve requires a non-null jsonb object for p_modifications.'
      USING ERRCODE = '22023';
  END IF;

  v_actor := lensers.get_auth_human_lenser_id();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_existing
  FROM agents.team_runs
  WHERE id = p_team_run_id
  FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Approval request % not found.', p_team_run_id
      USING ERRCODE = '42501';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_existing.ai_lenser_id) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI workspace.'
      USING ERRCODE = '42501';
  END IF;

  IF v_existing.approval_status <> 'pending' THEN
    RAISE EXCEPTION 'Request % is in approval_status="%", cannot decide twice.',
      p_team_run_id, v_existing.approval_status
      USING ERRCODE = '22023';
  END IF;

  v_new_metadata := COALESCE(v_existing.metadata, '{}'::jsonb)
    || jsonb_build_object(
         'decision_at', v_decision_at,
         'decision_by_lenser_id', v_actor,
         'decision_reason', p_reason
       );

  IF p_decision = 'modified' THEN
    v_new_metadata := v_new_metadata
      || jsonb_build_object('decision_modifications', p_modifications);
    v_event_type := 'approval_modified';
  ELSIF p_decision = 'approved' THEN
    v_event_type := 'approval_granted';
  ELSE
    v_event_type := 'approval_rejected';
  END IF;

  -- Approved-or-modified resumes the run by flipping team_run + workflow_run
  -- status. Rejected terminates both with `failed`.
  IF p_decision IN ('approved', 'modified') THEN
    UPDATE agents.team_runs
    SET approval_status = CASE WHEN p_decision = 'modified' THEN 'approved' ELSE p_decision END,
        status = 'queued',
        metadata = v_new_metadata,
        updated_at = v_decision_at
    WHERE id = p_team_run_id;

    IF v_existing.workflow_run_id IS NOT NULL THEN
      -- Recovery sweeper picks up `queued` runs; we do not call the engine
      -- directly. Only flip out of `blocked` so we never override an active
      -- transition that another process may have written.
      UPDATE lenses.workflow_runs
      SET status = 'queued'
      WHERE id = v_existing.workflow_run_id
        AND status IN ('pending', 'queued');
    END IF;

    v_terminal_run := 'queued';
  ELSE
    UPDATE agents.team_runs
    SET approval_status = 'rejected',
        status = 'failed',
        completed_at = v_decision_at,
        metadata = v_new_metadata,
        updated_at = v_decision_at
    WHERE id = p_team_run_id;

    IF v_existing.workflow_run_id IS NOT NULL THEN
      UPDATE lenses.workflow_runs
      SET status = 'failed',
          completed_at = COALESCE(completed_at, v_decision_at)
      WHERE id = v_existing.workflow_run_id
        AND status NOT IN ('completed', 'cancelled', 'failed');
    END IF;

    v_terminal_run := 'failed';
  END IF;

  INSERT INTO agents.agent_run_events (team_run_id, event_type, payload, occurred_at)
  VALUES (
    p_team_run_id,
    v_event_type,
    jsonb_build_object(
      'decision', p_decision,
      'reason', p_reason,
      'modifications', p_modifications,
      'decided_by_lenser_id', v_actor,
      'previous_run_status', v_existing.status,
      'next_run_status', v_terminal_run
    ),
    v_decision_at
  );

  RETURN QUERY
  SELECT
    p_team_run_id                                               AS request_id,
    v_existing.ai_lenser_id                                     AS ai_lenser_id,
    v_existing.team_id                                          AS team_id,
    v_existing.workflow_id                                      AS workflow_id,
    v_existing.workflow_run_id                                  AS workflow_run_id,
    v_existing.workflow_assignment_id                           AS workflow_assignment_id,
    CASE WHEN p_decision IN ('approved', 'modified') THEN 'approved' ELSE 'rejected' END AS approval_status,
    v_terminal_run                                              AS run_status,
    v_new_metadata                                              AS metadata,
    v_decision_at                                               AS decided_at;
END;
$$;


ALTER FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") IS 'Atomic owner-decision RPC for approval gates. Writes approval_status, merges decision audit fields into metadata, appends an agent_run_events row, and transitions the underlying workflow_run.';



CREATE OR REPLACE FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'audit', 'lensers'
    AS $$
DECLARE
  v_lenser_id        uuid;
  v_creator_lenser_id uuid;
  v_decision_type    text;
  v_new_status       battles.submission_status_enum;
BEGIN
  IF p_decision NOT IN ('allow', 'reject') THEN
    RAISE EXCEPTION 'Invalid decision: must be ''allow'' or ''reject''';
  END IF;

  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  -- Verify caller is the battle creator
  SELECT creator_lenser_id INTO v_creator_lenser_id
  FROM battles.battles
  WHERE id = p_battle_id;

  IF v_creator_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Battle not found: %', p_battle_id;
  END IF;

  IF v_creator_lenser_id <> v_lenser_id THEN
    RAISE EXCEPTION 'Only the battle creator may override moderation decisions'
      USING ERRCODE = '42501';
  END IF;

  -- Map decision to enum values
  IF p_decision = 'allow' THEN
    v_decision_type := 'restored';
    v_new_status    := 'submitted';
  ELSE
    v_decision_type := 'rejected';
    v_new_status    := 'disqualified';
  END IF;

  -- Update the submission status
  UPDATE battles.submissions
  SET status = v_new_status,
      updated_at = now()
  WHERE id = p_entry_id
    AND battle_id = p_battle_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Submission not found: %', p_entry_id;
  END IF;

  -- Audit the decision
  INSERT INTO audit.moderation_decisions (
    target_entity_schema,
    target_entity_table,
    target_entity_id,
    decision_type,
    reason,
    moderator_lenser_id,
    is_ai_moderated
  ) VALUES (
    'battles',
    'submissions',
    p_entry_id,
    v_decision_type,
    p_reason,
    v_lenser_id,
    false
  );
END;
$$;


ALTER FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'platform', 'lenses', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_auth_lenser_id uuid;
  v_log            platform.tool_invocation_logs%ROWTYPE;
BEGIN
  IF p_decision NOT IN ('approved', 'rejected') THEN
    RAISE EXCEPTION 'Invalid decision "%" — must be ''approved'' or ''rejected''', p_decision
      USING ERRCODE = '22023';
  END IF;

  v_auth_lenser_id := lensers.get_auth_lenser_id();

  SELECT * INTO v_log
  FROM platform.tool_invocation_logs
  WHERE id = p_log_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'tool_invocation_log % not found', p_log_id
      USING ERRCODE = 'P0002';
  END IF;

  -- Caller must own the ai_lenser that produced this log entry
  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    WHERE al.id = v_log.ai_lenser_id
      AND al.profile_id = v_auth_lenser_id
  ) THEN
    RAISE EXCEPTION 'Not authorised to decide on tool invocation log %', p_log_id
      USING ERRCODE = '42501';
  END IF;

  IF v_log.approval_status <> 'awaiting_approval' THEN
    RAISE EXCEPTION 'Cannot decide on log % with status "%"', p_log_id, v_log.approval_status
      USING ERRCODE = '22023';
  END IF;

  UPDATE platform.tool_invocation_logs
  SET
    approval_status = p_decision,
    decided_at      = now(),
    decided_by      = v_auth_lenser_id
  WHERE id = p_log_id;

  -- Emit a tool_decision event into the run timeline if the log is tied to a run
  IF v_log.run_id IS NOT NULL THEN
    INSERT INTO lenses.workflow_run_events (run_id, type, payload)
    VALUES (
      v_log.run_id,
      'tool_decision',
      jsonb_build_object(
        'log_id',    p_log_id,
        'tool_name', v_log.tool_name,
        'decision',  p_decision,
        'reason',    p_reason,
        'decided_by', v_auth_lenser_id,
        'decided_at', now()
      )
    );
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") IS 'Records approve/reject decision on platform.tool_invocation_logs and emits a tool_decision event to lenses.workflow_run_events for run timeline visibility.';



CREATE OR REPLACE FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
declare
  v_lenser_id uuid;
  v_encrypted bytea;
  v_key bytea;
  v_decrypted text;
begin
  -- Verify ownership
  select lenser_id, encrypted_config
  into v_lenser_id, v_encrypted
  from public.workflow_integration_credentials
  where id = p_credential_id;

  if v_lenser_id is null then
    raise exception 'Credential not found';
  end if;

  if v_lenser_id != (select id from lensers.profiles where user_id = auth.uid()) then
    raise exception 'Access denied';
  end if;

  -- Decrypt using the platform encryption key from vault
  select decrypted_secret into v_key
  from vault.decrypted_secrets
  where name = 'workflow_integration_encryption_key'
  limit 1;

  if v_key is null then
    raise exception 'Encryption key not configured';
  end if;

  v_decrypted := pgp_sym_decrypt(v_encrypted, encode(v_key, 'hex'));
  return v_decrypted::jsonb;
end;
$$;


ALTER FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_delete('agent', p_ai_lenser_id); $$;


ALTER FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  DELETE FROM automation.trigger_rules
  WHERE  id        = p_rule_id
    AND  lenser_id = auth.uid();
$$;


ALTER FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") IS 'Security wrapper: delete a trigger rule owned by the current user. Silently no-ops if the rule does not belong to the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
BEGIN
  PERFORM public.fn_artifact_delete('lens', p_lens_id);
END;
$$;


ALTER FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'audit', 'public'
    AS $$
DECLARE
  v_owner_id UUID;
BEGIN
  SELECT owner_lenser_id INTO v_owner_id
  FROM media.objects
  WHERE id = p_object_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'media_object_not_found: %', p_object_id USING ERRCODE = 'P0001';
  END IF;

  IF v_owner_id != auth.uid() THEN
    RAISE EXCEPTION 'media_delete_forbidden: caller does not own object %', p_object_id
      USING ERRCODE = 'P0001';
  END IF;

  UPDATE media.objects
  SET lifecycle_state = 'deleted'
  WHERE id = p_object_id;

  INSERT INTO audit.events (event_type, actor_type, severity, payload)
  VALUES (
    'media.object_deleted',
    'user',
    'info',
    jsonb_build_object('object_id', p_object_id, 'deleted_by', auth.uid(), 'deleted_at', now())
  );
END;
$$;


ALTER FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") IS 'AT: Soft-deletes a media object (lifecycle_state=deleted). Owner only. Writes audit.events row. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  DELETE FROM content.threads
  WHERE id        = p_thread_id
    AND lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") IS 'Security wrapper: delete a thread owned by the current user. Silently no-ops if not owned.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  DELETE FROM content.thread_replies
  WHERE id        = p_reply_id
    AND lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") IS 'Security wrapper: delete a thread reply owned by the current user. Silently no-ops if not owned.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_delete('workflow', p_workflow_id); $$;


ALTER FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflow_edges e
    JOIN lenses.workflows w ON w.id = e.workflow_id
    WHERE e.id = p_edge_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized or edge not found: %', p_edge_id
      USING ERRCODE = '42501';
  END IF;

  DELETE FROM lenses.workflow_edges WHERE id = p_edge_id;
END;
$$;


ALTER FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") IS 'Deletes a workflow edge the caller owns.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflow_nodes n
    JOIN lenses.workflows w ON w.id = n.workflow_id
    WHERE n.id = p_node_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized or node not found: %', p_node_id
      USING ERRCODE = '42501';
  END IF;

  DELETE FROM lenses.workflow_nodes WHERE id = p_node_id;
END;
$$;


ALTER FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") IS 'Deletes a workflow node the caller owns.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  DELETE FROM lenses.workflow_phases wp
  USING lenses.workflows w
  WHERE wp.id = p_phase_id
    AND wp.workflow_id = w.id
    AND w.lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  DELETE FROM lenses.workflow_schedules
  WHERE id        = p_schedule_id
    AND workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") IS 'Security wrapper: delete a workflow schedule owned by the current user. Silently no-ops if not found or not owned by caller.';



CREATE OR REPLACE FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  DELETE FROM lenses.workflow_tasks wt
  USING lenses.workflows w
  WHERE wt.id = p_task_id
    AND wt.workflow_id = w.id
    AND w.lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $_$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
  v_predicate text;
BEGIN
  IF p_table_name NOT IN (
    'teams', 'team_members', 'team_edges', 'personality_profiles',
    'memory_profiles', 'tool_profiles', 'model_profiles',
    'workflow_assignments', 'evaluation_cases', 'evaluation_rubrics',
    'evaluation_baselines', 'tools_registry', 'tool_assignments',
    'evaluations', 'scratchpad_runs', 'workspace_settings',
    'agent_run_events', 'agent_run_steps'
  ) THEN
    RAISE EXCEPTION 'delete_forbidden: table % not in allowlist', p_table_name
      USING ERRCODE = '42501';
  END IF;

  v_predicate := public.fn__workspace_ownership_predicate(p_table_name);
  IF v_predicate IS NULL THEN
    RAISE EXCEPTION 'delete_forbidden: no ownership predicate for %', p_table_name
      USING ERRCODE = '42501';
  END IF;

  EXECUTE format(
    'DELETE FROM agents.%I tbl WHERE tbl.id = $1 AND (%s OR public.is_admin())',
    p_table_name,
    v_predicate
  ) USING p_id, v_lenser_id;
END;
$_$;


ALTER FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") IS 'Delete an agents workspace record. Ownership: ai_lenser_id via agents.ownerships, or team_id via teams→ownerships, or is_admin. No reference to non-existent owner_lenser_id column.';



CREATE OR REPLACE FUNCTION "public"."fn_deny_mutation"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO 'public'
    AS $$
BEGIN
  RAISE EXCEPTION '% is append-only and cannot be %', TG_TABLE_NAME, TG_OP;
END;
$$;


ALTER FUNCTION "public"."fn_deny_mutation"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_deny_mutation"() IS 'Generic append-only guard. Attach as BEFORE UPDATE/DELETE trigger on any table that must be insert-only (rule_snapshots, action_logs, audit chains, etc.). Raises an exception on any mutation attempt.';



CREATE OR REPLACE FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'auth', 'public'
    AS $$
DECLARE
  rec RECORD;
BEGIN
  -- Pattern: new account (<24h old)
  FOR rec IN
    SELECT DISTINCT v.voter_lenser_id
      FROM battles.votes v
      JOIN lensers.profiles p ON p.id = v.voter_lenser_id
      JOIN auth.users u ON u.id = p.user_id
     WHERE v.battle_id = p_battle_id
       AND u.created_at >= now() - INTERVAL '24 hours'
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'new_account',
      'score',    0.7
    );
  END LOOP;

  -- Pattern: 0 battles participated (voter never submitted an entry)
  FOR rec IN
    SELECT DISTINCT v.voter_lenser_id
      FROM battles.votes v
     WHERE v.battle_id = p_battle_id
       AND NOT EXISTS (
         SELECT 1 FROM battles.contenders c
          WHERE c.contender_ref_id = v.voter_lenser_id
       )
       AND NOT EXISTS (
         SELECT 1 FROM battles.votes v2
          WHERE v2.voter_lenser_id = v.voter_lenser_id
            AND v2.battle_id <> p_battle_id
       )
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'zero_battles_participated',
      'score',    0.5
    );
  END LOOP;

  -- Pattern: >80% votes to single contender
  FOR rec IN
    SELECT
      v.voter_lenser_id,
      MAX(cnt)::float / NULLIF(SUM(cnt)::float, 0) AS concentration
    FROM (
      SELECT voter_lenser_id, voted_contender_id, COUNT(*) AS cnt
        FROM battles.votes
       WHERE battle_id = p_battle_id
       GROUP BY voter_lenser_id, voted_contender_id
    ) v
    GROUP BY v.voter_lenser_id
    HAVING MAX(cnt)::float / NULLIF(SUM(cnt)::float, 0) > 0.8
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'high_concentration',
      'score',    LEAST(1.0, rec.concentration)
    );
  END LOOP;

  -- Pattern: votes spaced <2s apart
  FOR rec IN
    SELECT DISTINCT v1.voter_lenser_id
      FROM battles.votes v1
      JOIN battles.votes v2
        ON v2.voter_lenser_id = v1.voter_lenser_id
       AND v2.battle_id       = v1.battle_id
       AND v2.id             <> v1.id
       AND ABS(EXTRACT(EPOCH FROM (v2.created_at - v1.created_at))) < 2
     WHERE v1.battle_id = p_battle_id
  LOOP
    RETURN NEXT jsonb_build_object(
      'voter_lenser_id', rec.voter_lenser_id,
      'reason',   'rapid_succession',
      'score',    0.8
    );
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") IS 'CA: Emits anomaly signals for a battle. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_approve(p_device_id);
$$;


ALTER FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text" DEFAULT NULL::"text", "p_envelope_sig" "text" DEFAULT NULL::"text", "p_gateway_status" "text" DEFAULT 'connected'::"text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_heartbeat(
    p_device_id, p_daemon_version, p_envelope_sig, p_gateway_status
  );
$$;


ALTER FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_list"("p_trust_level" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 50, "p_cursor" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "name" "text", "device_type" "text", "os" "text", "arch" "text", "cli_version" "text", "runner_version" "text", "capabilities" "jsonb", "trust_level" "text", "gateway_status" "text", "last_seen_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_device_list(p_trust_level, p_limit, p_cursor);
$$;


ALTER FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone DEFAULT "now"()) RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_post_challenge(p_device_id, p_signature, p_signed_iat);
$$;


ALTER FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text" DEFAULT 'other'::"text", "p_os" "text" DEFAULT NULL::"text", "p_arch" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_capabilities" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_register(
    p_name, p_device_type, p_os, p_arch, p_cli_version, p_capabilities
  );
$$;


ALTER FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text" DEFAULT 'other'::"text", "p_os" "text" DEFAULT NULL::"text", "p_arch" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_daemon_version" "text" DEFAULT NULL::"text", "p_capabilities" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("device_id" "uuid", "challenge_id" "uuid", "challenge_nonce" "text", "challenge_expires_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_device_register_with_key(
    p_name, p_public_key, p_device_type, p_os, p_arch,
    p_cli_version, p_daemon_version, p_capabilities
  );
$$;


ALTER FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT devices.fn_device_revoke(p_device_id);
$$;


ALTER FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'agents', 'platform', 'public'
    AS $$
DECLARE
  v_dispatch_enabled boolean;
  v_result           integer;
BEGIN
  -- Check platform-level autonomy kill switch (populated by Phase 2.9 migration).
  -- If the table or row does not yet exist, default to enabled.
  BEGIN
    SELECT (value::boolean) INTO v_dispatch_enabled
    FROM platform.system_flags
    WHERE key = 'autonomy_dispatch_enabled';
  EXCEPTION WHEN undefined_table OR undefined_column THEN
    v_dispatch_enabled := true;
  END;

  IF v_dispatch_enabled IS NOT DISTINCT FROM false THEN
    RAISE NOTICE 'Autonomous dispatch is disabled (platform.system_flags.autonomy_dispatch_enabled=false). Skipping.';
    RETURN 0;
  END IF;

  -- requiresApproval=false AND spending_limit_credits IS NULL guard:
  -- Log a warning for any schedule that bypasses approval without a spending cap.
  -- The UPSERT guard above should prevent these from being created, but we
  -- audit here as a belt-and-suspenders check.
  PERFORM 1
  FROM lenses.workflow_schedules s
  JOIN lenses.workflows w ON w.id = s.workflow_id
  JOIN agents.ai_lensers al ON al.profile_id = w.lenser_id
  JOIN agents.policies pol ON pol.id = al.policy_id
  WHERE s.is_active = true
    AND COALESCE((s.approval_policy->>'requiresApproval')::boolean, true) = false
    AND pol.spending_limit_credits IS NULL;

  IF FOUND THEN
    RAISE WARNING
      'One or more active schedules have requiresApproval=false and no spending_limit_credits. '
      'These schedules will dispatch without approval gates and without a cost ceiling. '
      'Set a spending limit or re-enable approval to suppress this warning.';
  END IF;

  -- Delegate to the existing approval-aware dispatcher (lenses schema).
  SELECT lenses.fn_dispatch_scheduled_workflows() INTO v_result;
  RETURN COALESCE(v_result, 0);
END;
$$;


ALTER FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() IS 'Phase 2 pg_cron target. Wraps lenses.fn_dispatch_scheduled_workflows() with a platform-level autonomy kill switch check and an approval/spending-limit audit warning. The kill switch is controlled via platform.system_flags.autonomy_dispatch_enabled.';



CREATE OR REPLACE FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean DEFAULT false) RETURNS TABLE("switch_id" "uuid", "cancelled_runs" integer, "cancelled_jobs" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_switch_id UUID;
  v_run_count INT := 0;
  v_job_count INT := 0;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'emergency_stop_forbidden: admin role required';
  END IF;

  -- Activate a system-wide kill switch. fn_kill_switch_activate is SECURITY DEFINER
  -- and checks is_admin() internally; it also writes its own audit.events row.
  v_switch_id := public.fn_kill_switch_activate('system', NULL, p_reason, NULL);

  -- Optional: mass-cancel all in-flight runs and jobs.
  IF p_force_mode THEN
    SELECT c.cancelled_run_count, c.cancelled_job_count
    INTO   v_run_count, v_job_count
    FROM   public.fn_cancel_all_active_runs(p_reason) c;
  END IF;

  RETURN QUERY SELECT v_switch_id, v_run_count, v_job_count;
END;
$$;


ALTER FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) IS 'Admin-only emergency stop. Activates a system-wide kill switch that blocks all worker claim and schedule dispatch calls. When force_mode=true, also calls fn_cancel_all_active_runs to cancel in-flight runs and battle jobs immediately. Returns (switch_id, cancelled_runs, cancelled_jobs).';



CREATE OR REPLACE FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("verdict" "text", "reason" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_settings        agents.workspace_settings;
  v_credits_used    numeric;
  v_active_runs     integer;
  v_workflow_hash   integer;
  v_verdict         text;
  v_reason          text;
  v_policy_type     text;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  -- Load or default settings
  SELECT * INTO v_settings
    FROM agents.workspace_settings
   WHERE ai_lenser_id = p_ai_lenser_id;

  IF NOT FOUND THEN
    -- All defaults: allow
    v_verdict     := 'allow';
    v_reason      := NULL;
    v_policy_type := 'kill_switch';

    INSERT INTO agents.policy_evaluations (
      ai_lenser_id, team_run_id, tool_invocation_id,
      evaluation_point, policy_type, verdict, reason, context, evaluated_at
    ) VALUES (
      p_ai_lenser_id, NULL, NULL,
      'pre_run', v_policy_type, v_verdict, v_reason,
      COALESCE(p_context, '{}'::jsonb), now()
    );

    RETURN QUERY SELECT v_verdict, v_reason;
    RETURN;
  END IF;

  -- 1. Global kill switch
  IF v_settings.global_kill_switch THEN
    v_verdict     := 'deny';
    v_reason      := 'global kill switch active';
    v_policy_type := 'kill_switch';

  -- 2. Agent paused
  ELSIF v_settings.agent_paused THEN
    v_verdict     := 'pause';
    v_reason      := 'agent is paused';
    v_policy_type := 'pause';

  -- 3. Daily budget
  ELSIF v_settings.budget_enforce THEN
    SELECT COALESCE(SUM(credits_spent), 0)
      INTO v_credits_used
      FROM agents.quota_snapshots
     WHERE ai_lenser_id = p_ai_lenser_id
       AND period_date  = CURRENT_DATE;

    IF v_credits_used >= v_settings.max_daily_credits THEN
      v_verdict     := 'deny';
      v_reason      := 'daily budget exceeded';
      v_policy_type := 'budget';
    END IF;
  END IF;

  -- 4. Parallel run limit (only if not already denied above)
  IF v_verdict IS NULL THEN
    SELECT COUNT(*)
      INTO v_active_runs
      FROM agents.team_runs
     WHERE ai_lenser_id = p_ai_lenser_id
       AND status IN ('queued','running','blocked');

    IF v_active_runs >= v_settings.max_parallel_runs THEN
      v_verdict     := 'deny';
      v_reason      := 'max_parallel_runs limit reached';
      v_policy_type := 'parallel_limit';
    END IF;
  END IF;

  -- 5. Dark launch gating
  IF v_verdict IS NULL
     AND v_settings.dark_launch_enabled
     AND p_workflow_id IS NOT NULL
  THEN
    v_workflow_hash := (('x' || md5(p_workflow_id::text))::bit(32)::int & 2147483647) % 100;
    IF v_workflow_hash >= v_settings.dark_launch_pct THEN
      v_verdict     := 'deny';
      v_reason      := 'outside dark launch percentage';
      v_policy_type := 'dark_launch';
    END IF;
  END IF;

  -- 6. Approval default: require_human
  IF v_verdict IS NULL AND v_settings.approval_default = 'require_human' THEN
    v_verdict     := 'require_approval';
    v_reason      := 'workspace default requires human approval';
    v_policy_type := 'approval';
  END IF;

  -- 7. Approval default: deny-all
  IF v_verdict IS NULL AND v_settings.approval_default = 'deny' THEN
    v_verdict     := 'deny';
    v_reason      := 'workspace approval policy is deny-all';
    v_policy_type := 'approval';
  END IF;

  -- 8. Default: allow
  IF v_verdict IS NULL THEN
    v_verdict     := 'allow';
    v_reason      := NULL;
    v_policy_type := 'kill_switch';
  END IF;

  -- Always audit
  INSERT INTO agents.policy_evaluations (
    ai_lenser_id, team_run_id, tool_invocation_id,
    evaluation_point, policy_type, verdict, reason, context, evaluated_at
  ) VALUES (
    p_ai_lenser_id, NULL, NULL,
    'pre_run', v_policy_type, v_verdict, v_reason,
    COALESCE(p_context, '{}'::jsonb), now()
  );

  RETURN QUERY SELECT v_verdict, v_reason;
END;
$$;


ALTER FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text" DEFAULT 'succeeded'::"text", "p_response_text" "text" DEFAULT NULL::"text", "p_response_meta" "jsonb" DEFAULT '{}'::"jsonb", "p_token_input" integer DEFAULT 0, "p_token_output" integer DEFAULT 0, "p_credit_cost" bigint DEFAULT 0) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
DECLARE
    v_request_id     uuid;
    v_run_id         uuid;
    v_model_id       uuid;
    v_billing_status text;
BEGIN
    -- Resolve model_id from provider + model key (best-effort)
    SELECT m.id INTO v_model_id
    FROM ai.models m
    JOIN ai.providers p ON p.id = m.provider_id
    WHERE p.key = p_provider AND m.key = p_model
    LIMIT 1;

    v_billing_status := CASE WHEN p_credit_cost > 0 THEN 'charged' ELSE 'free' END;

    -- Create request row
    INSERT INTO execution.requests (
        requester_lenser_id,
        origin_type,
        model_id,
        funding_source,
        runtime_origin
    ) VALUES (
        p_lenser_id,
        'api',
        v_model_id,
        CASE WHEN p_credit_cost > 0 THEN 'platform_credit' ELSE 'free' END,
        'cloud'
    )
    RETURNING id INTO v_request_id;

    -- Create run row with response
    INSERT INTO execution.runs (
        request_id,
        status,
        model_id,
        token_input,
        token_output,
        credit_cost,
        billing_status,
        response_text,
        response_meta,
        started_at,
        completed_at
    ) VALUES (
        v_request_id,
        p_status,
        v_model_id,
        p_token_input,
        p_token_output,
        p_credit_cost,
        v_billing_status,
        p_response_text,
        p_response_meta,
        now(),
        CASE WHEN p_status IN ('succeeded', 'failed', 'timed_out') THEN now() ELSE NULL END
    )
    RETURNING id INTO v_run_id;

    RETURN v_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) IS 'Persists an AI execution response by creating request + run rows. Called fire-and-forget by the API Worker after provider calls complete.';



CREATE OR REPLACE FUNCTION "public"."fn_expire_byok_keys"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'audit', 'public'
    AS $$
DECLARE
  v_count INT;
BEGIN
  UPDATE execution.byok_keys
  SET revoked_at = now()
  WHERE expires_at < now()
    AND revoked_at IS NULL;

  GET DIAGNOSTICS v_count = ROW_COUNT;

  IF v_count > 0 THEN
    INSERT INTO audit.events (event_type, actor_type, severity, payload)
    VALUES (
      'byok.keys_expired',
      'system',
      'info',
      jsonb_build_object('expired_count', v_count, 'expired_at', now())
    );
  END IF;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_expire_byok_keys"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_expire_byok_keys"() IS 'AR: Soft-revokes all BYOK keys where expires_at < now(). Called by pg_cron every hour. Returns number of keys revoked. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_expire_media_objects"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'audit', 'public'
    AS $$
DECLARE
  v_count INT;
BEGIN
  UPDATE media.objects
  SET lifecycle_state = 'archived'
  WHERE expires_at < now()
    AND lifecycle_state NOT IN ('deleted', 'archived');

  GET DIAGNOSTICS v_count = ROW_COUNT;

  IF v_count > 0 THEN
    INSERT INTO audit.events (event_type, actor_type, severity, payload)
    VALUES (
      'media.objects_expired',
      'system',
      'info',
      jsonb_build_object('archived_count', v_count, 'archived_at', now())
    );
  END IF;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_expire_media_objects"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_expire_media_objects"() IS 'AT: Sets lifecycle_state=archived for objects past expires_at. Called by pg_cron daily at 02:00. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_expire_stale_approvals"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lenses', 'public'
    AS $$
DECLARE
  v_threshold_hours integer := COALESCE(
    NULLIF(current_setting('app.approval_timeout_hours', true), '')::integer,
    24
  );
  v_cutoff   timestamptz := now() - (v_threshold_hours * interval '1 hour');
  v_expired  integer     := 0;
  r          RECORD;
BEGIN
  FOR r IN
    SELECT id, workflow_run_id
    FROM   agents.team_runs
    WHERE  approval_status = 'pending'
      AND  created_at < v_cutoff
    FOR UPDATE SKIP LOCKED
  LOOP
    BEGIN
      UPDATE agents.team_runs
      SET    approval_status = 'timed_out',
             status          = 'cancelled',
             completed_at    = COALESCE(completed_at, now()),
             metadata        = COALESCE(metadata, '{}'::jsonb)
                                 || jsonb_build_object(
                                      'timed_out_at', now(),
                                      'timeout_hours', v_threshold_hours
                                    ),
             updated_at      = now()
      WHERE  id = r.id;

      IF r.workflow_run_id IS NOT NULL THEN
        UPDATE lenses.workflow_runs
        SET    status        = 'timed_out',
               completed_at  = COALESCE(completed_at, now())
        WHERE  id = r.workflow_run_id
          AND  status NOT IN ('completed', 'cancelled', 'failed', 'timed_out');
      END IF;

      INSERT INTO agents.agent_run_events (team_run_id, event_type, payload, occurred_at)
      VALUES (
        r.id,
        'approval_timed_out',
        jsonb_build_object(
          'timeout_hours',     v_threshold_hours,
          'workflow_run_id',   r.workflow_run_id,
          'expired_at',        now()
        ),
        now()
      );

      v_expired := v_expired + 1;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_expire_stale_approvals: team_run % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN v_expired;
END;
$$;


ALTER FUNCTION "public"."fn_expire_stale_approvals"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_expire_stale_approvals"() IS 'Phase K1: pg_cron target. Expires pending team_runs older than app.approval_timeout_hours (default 24h). Writes approval_status=timed_out, cancels the underlying workflow_run with status=timed_out, and emits an approval_timed_out event. Idempotent and concurrency-safe.';



CREATE OR REPLACE FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_bundle jsonb;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  SELECT jsonb_build_object(
    'ai_lenser',        to_jsonb(al),
    'settings',         to_jsonb(ws),
    'teams',            COALESCE((SELECT jsonb_agg(to_jsonb(t))  FROM agents.teams t  WHERE t.ai_lenser_id = al.id), '[]'::jsonb),
    'memory_profiles',  COALESCE((SELECT jsonb_agg(to_jsonb(m))  FROM agents.memory_profiles m WHERE m.ai_lenser_id = al.id), '[]'::jsonb),
    'personality',      COALESCE((SELECT jsonb_agg(to_jsonb(p))  FROM agents.personality_profiles p WHERE p.ai_lenser_id = al.id), '[]'::jsonb),
    'tool_profiles',    COALESCE((SELECT jsonb_agg(to_jsonb(tp)) FROM agents.tool_profiles tp WHERE tp.ai_lenser_id = al.id), '[]'::jsonb),
    'model_profiles',   COALESCE((SELECT jsonb_agg(to_jsonb(mp)) FROM agents.model_profiles mp WHERE mp.ai_lenser_id = al.id), '[]'::jsonb),
    'tool_assignments', COALESCE((SELECT jsonb_agg(to_jsonb(ta)) FROM agents.tool_assignments ta WHERE ta.ai_lenser_id = al.id), '[]'::jsonb),
    'exported_at',      now()
  )
  INTO v_bundle
  FROM agents.ai_lensers al
  LEFT JOIN agents.workspace_settings ws ON ws.ai_lenser_id = al.id
  WHERE al.id = p_ai_lenser_id;

  RETURN v_bundle;
END;
$$;


ALTER FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
BEGIN
  INSERT INTO audit.vote_anomalies (battle_id, voter_lenser_id, anomaly_type, score)
  VALUES (p_battle_id, p_voter_id, p_type, p_score);
END;
$$;


ALTER FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) IS 'CA: Inserts a vote anomaly record. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) RETURNS integer
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_ack_commands(p_command_ids);
$$;


ALTER FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_approve_device(p_device_id);
$$;


ALTER FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS SETOF "agents"."gateway_commands"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT * FROM agents.fn_gateway_claim_commands(p_device_id, p_limit);
$$;


ALTER FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) IS 'Deprecated since Phase BG. Use public.fn_gateway_claim_commands_v2.';



CREATE OR REPLACE FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS TABLE("id" "uuid", "device_id" "uuid", "command_type" "text", "payload" "jsonb", "created_at" timestamp with time zone, "claimed_at" timestamp with time zone, "acked_at" timestamp with time zone, "envelope_sig" "text", "envelope_nonce" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT * FROM agents.fn_gateway_claim_commands_v2(p_device_id, p_limit);
$$;


ALTER FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text" DEFAULT NULL::"text", "p_daemon_version" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_heartbeat(p_device_id, p_public_key, p_hostname, p_daemon_version);
$$;


ALTER FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT agents.fn_gateway_revoke_device(p_device_id);
$$;


ALTER FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid" DEFAULT '00000000-0000-0000-0000-000000000001'::"uuid") RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "starts_at" timestamp with time zone, "ends_at" timestamp with time zone, "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
  SELECT id, slug, name, starts_at, ends_at, is_active
  FROM xp.seasons
  WHERE app_id = p_app_id
    AND is_active = true
    AND now() BETWEEN starts_at AND ends_at
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer DEFAULT 30, "p_model_key" "text" DEFAULT NULL::"text", "p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'agents', 'lenses'
    AS $$
DECLARE
  v_cost_by_model    jsonb;
  v_cost_time_series jsonb;
  v_eval_quality     jsonb;
  v_workflow_perf    jsonb;
  v_jwt_role text := coalesce(nullif(trim(both from current_setting('request.jwt.claim.role', true)), ''), '');
BEGIN
  -- Ownership guard: human owners pass agents.can_manage_ai_lenser.
  -- Service-role JWT (platform / trusted workers) may read any agent aggregates.
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    IF v_jwt_role IS DISTINCT FROM 'service_role' THEN
      RAISE EXCEPTION 'Insufficient privilege'
        USING ERRCODE = '42501';
    END IF;
  END IF;

  SELECT jsonb_agg(row_to_json(t)) INTO v_cost_by_model
  FROM (
    SELECT
      model_key,
      provider,
      SUM(total_credits)    AS total_credits,
      SUM(total_tokens_in)  AS total_tokens_in,
      SUM(total_tokens_out) AS total_tokens_out,
      SUM(run_count)        AS run_count
    FROM analytics.agent_cost_daily
    WHERE ai_lenser_id = p_ai_lenser_id
      AND period_date  >= CURRENT_DATE - p_days
      AND (p_model_key IS NULL OR model_key = p_model_key)
    GROUP BY model_key, provider
    ORDER BY SUM(total_credits) DESC
  ) t;

  SELECT jsonb_agg(row_to_json(t)) INTO v_cost_time_series
  FROM (
    SELECT
      period_date,
      SUM(total_credits) AS total_credits
    FROM analytics.agent_cost_daily
    WHERE ai_lenser_id = p_ai_lenser_id
      AND period_date  >= CURRENT_DATE - p_days
      AND (p_model_key IS NULL OR model_key = p_model_key)
    GROUP BY period_date
    ORDER BY period_date
  ) t;

  SELECT jsonb_agg(row_to_json(t)) INTO v_eval_quality
  FROM (
    SELECT
      eqd.period_date,
      e.name                                            AS evaluation_name,
      eqd.pass_count::float / NULLIF(eqd.run_count, 0) AS pass_rate,
      eqd.mean_score
    FROM analytics.eval_quality_daily eqd
    JOIN agents.evaluations e ON e.id = eqd.evaluation_id
    WHERE eqd.ai_lenser_id = p_ai_lenser_id
      AND eqd.period_date  >= CURRENT_DATE - p_days
    ORDER BY eqd.period_date
  ) t;

  SELECT jsonb_agg(row_to_json(t)) INTO v_workflow_perf
  FROM (
    SELECT
      wpd.period_date,
      wf.title                                              AS workflow_title,
      wpd.p50_duration_ms,
      wpd.p95_duration_ms,
      wpd.failed_count::float / NULLIF(wpd.run_count, 0)   AS failure_rate
    FROM analytics.workflow_perf_daily wpd
    JOIN lenses.workflows wf ON wf.id = wpd.workflow_id
    WHERE wpd.ai_lenser_id = p_ai_lenser_id
      AND wpd.period_date  >= CURRENT_DATE - p_days
      AND (p_workflow_id IS NULL OR wpd.workflow_id = p_workflow_id)
    ORDER BY wpd.period_date
  ) t;

  RETURN jsonb_build_object(
    'cost_by_model',    COALESCE(v_cost_by_model,    '[]'::jsonb),
    'cost_time_series', COALESCE(v_cost_time_series, '[]'::jsonb),
    'eval_quality',     COALESCE(v_eval_quality,     '[]'::jsonb),
    'workflow_perf',    COALESCE(v_workflow_perf,    '[]'::jsonb)
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") IS 'Owner-only analytics summary for an AI lenser (human owners via agents.can_manage_ai_lenser). Callers with request.jwt.claim.role = service_role may read aggregates for any agent (platform / trusted workers). Returns cost_by_model, cost_time_series, eval_quality, and workflow_perf aggregated over the last p_days days. Optional p_model_key and p_workflow_id narrow cost and workflow results. Reads only from analytics.* pre-aggregated tables; never touches raw logs.';



CREATE OR REPLACE FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 100, "p_offset" integer DEFAULT 0) RETURNS TABLE("kind" "text", "id" "text", "occurred_at" timestamp with time zone, "title" "text", "result" "text", "workflow_id" "uuid", "workflow_title" "text", "run_id" "uuid", "schedule_id" "uuid", "action_type" "text", "event_type" "text", "payload" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lenses', 'lensers'
    AS $$
  WITH authorized_agent AS (
    SELECT al.id AS ai_lenser_id, al.profile_id
    FROM agents.ai_lensers al
    JOIN agents.ownerships o ON o.ai_lenser_id = al.id
    WHERE al.id = p_ai_lenser_id
      AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND o.revoked_at IS NULL
    LIMIT 1
  ),
  action_items AS (
    SELECT
      CASE
        WHEN l.action_type IN ('dispatch_schedule', 'schedule_skipped') THEN 'schedule_dispatch'
        ELSE 'agent_action'
      END AS kind,
      l.id::text AS id,
      l.occurred_at,
      COALESCE(l.action_type, 'agent_action') AS title,
      l.result,
      CASE WHEN l.metadata ? 'workflow_id' THEN (l.metadata ->> 'workflow_id')::uuid END AS workflow_id,
      NULL::text AS workflow_title,
      CASE WHEN l.metadata ? 'run_id' THEN (l.metadata ->> 'run_id')::uuid END AS run_id,
      CASE WHEN l.metadata ? 'schedule_id' THEN (l.metadata ->> 'schedule_id')::uuid END AS schedule_id,
      l.action_type,
      NULL::text AS event_type,
      l.metadata AS payload
    FROM agents.action_logs l
    JOIN authorized_agent aa ON aa.ai_lenser_id = l.ai_lenser_id
  ),
  run_items AS (
    SELECT
      'workflow_run'::text AS kind,
      r.id::text AS id,
      COALESCE(r.completed_at, r.started_at, r.created_at) AS occurred_at,
      r.status AS title,
      r.status AS result,
      w.id AS workflow_id,
      w.title AS workflow_title,
      r.id AS run_id,
      r.schedule_id,
      NULL::text AS action_type,
      NULL::text AS event_type,
      jsonb_build_object(
        'status', r.status,
        'trigger_mode', r.trigger_mode,
        'spent_credits', r.spent_credits
      ) AS payload
    FROM authorized_agent aa
    JOIN lenses.workflows w ON w.lenser_id = aa.profile_id
    JOIN lenses.workflow_runs r ON r.workflow_id = w.id
  ),
  event_items AS (
    SELECT
      'workflow_event'::text AS kind,
      concat(e.run_id::text, ':', e.event_id::text) AS id,
      e.created_at AS occurred_at,
      e.type AS title,
      NULL::text AS result,
      w.id AS workflow_id,
      w.title AS workflow_title,
      e.run_id AS run_id,
      r.schedule_id,
      NULL::text AS action_type,
      e.type AS event_type,
      COALESCE(e.payload, '{}'::jsonb) AS payload
    FROM authorized_agent aa
    JOIN lenses.workflows w ON w.lenser_id = aa.profile_id
    JOIN lenses.workflow_runs r ON r.workflow_id = w.id
    JOIN lenses.workflow_run_events e ON e.run_id = r.id
  )
  SELECT *
  FROM (
    SELECT * FROM action_items
    UNION ALL
    SELECT * FROM run_items
    UNION ALL
    SELECT * FROM event_items
  ) feed
  ORDER BY occurred_at DESC, id DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 250)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Owner-only unified automation feed for AI workspace panels. Unions agents.action_logs with workflow_runs and workflow_run_events for workflows owned by the AI lenser profile.';



CREATE OR REPLACE FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") RETURNS TABLE("period_date" "date", "credits_spent" integer, "battles_used" integer, "votes_used" integer, "spending_limit" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT qs.period_date, qs.credits_spent, qs.battles_used, qs.votes_used,
         pol.spending_limit_credits
  FROM agents.quota_snapshots qs
  LEFT JOIN agents.policies pol ON pol.ai_lenser_id = qs.ai_lenser_id
  WHERE qs.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY qs.period_date DESC
  LIMIT 30;
$$;


ALTER FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(v.*)
  FROM agents.v_agent_profile v
  WHERE v.id = p_ai_lenser_id
    AND (
      v.owner_lenser_id = lensers.get_auth_human_lenser_id()
      OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(v.*)
  FROM agents.v_agent_profile v
  WHERE v.profile_id = p_profile_id
    AND (v.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date" DEFAULT CURRENT_DATE) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "period_date" "date", "battles_used" integer, "votes_used" integer, "credits_spent" integer, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT qs.id, qs.ai_lenser_id, qs.period_date, qs.battles_used,
         qs.votes_used, qs.credits_spent, qs.updated_at
  FROM agents.quota_snapshots qs
  WHERE qs.ai_lenser_id = p_ai_lenser_id
    AND qs.period_date  = p_period_date
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_profile_id uuid;
  v_ai_lenser_id uuid;
  v_owner_ok boolean;
BEGIN
  SELECT p.id, al.id
  INTO v_profile_id, v_ai_lenser_id
  FROM lensers.profiles p
  JOIN agents.ai_lensers al ON al.profile_id = p.id
  WHERE p.handle = p_profile_handle
    AND p.type = 'ai'
  LIMIT 1;

  IF v_ai_lenser_id IS NULL THEN
    RETURN NULL;
  END IF;

  SELECT agents.can_manage_ai_lenser(v_ai_lenser_id) INTO v_owner_ok;
  IF NOT v_owner_ok THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI workspace'
      USING ERRCODE = '42501';
  END IF;

  RETURN jsonb_build_object(
    'profile_id', v_profile_id,
    'ai_lenser_id', v_ai_lenser_id,
    'teams', COALESCE((
      SELECT jsonb_agg(
        jsonb_build_object(
          'id', t.id,
          'ai_lenser_id', t.ai_lenser_id,
          'name', t.name,
          'description', t.description,
          'status', t.status,
          'is_active', t.is_active,
          'scratchpad', t.scratchpad,
          'member_count', (
            SELECT count(*) FROM agents.team_members tm WHERE tm.team_id = t.id AND tm.is_active = true
          ),
          'members', COALESCE((
            SELECT jsonb_agg(tm ORDER BY tm.lane, tm.sort_order, tm.created_at)
            FROM agents.team_members tm
            WHERE tm.team_id = t.id
          ), '[]'::jsonb),
          'edges', COALESCE((
            SELECT jsonb_agg(te ORDER BY te.created_at)
            FROM agents.team_edges te
            WHERE te.team_id = t.id
          ), '[]'::jsonb),
          'created_at', t.created_at,
          'updated_at', t.updated_at
        )
        ORDER BY t.updated_at DESC
      )
      FROM agents.teams t
      WHERE t.ai_lenser_id = v_ai_lenser_id
    ), '[]'::jsonb),
    'runs', COALESCE((
      SELECT jsonb_agg(
        jsonb_build_object(
          'id', tr.id,
          'team_id', tr.team_id,
          'workflow_id', tr.workflow_id,
          'workflow_run_id', tr.workflow_run_id,
          'status', tr.status,
          'approval_status', tr.approval_status,
          'started_at', tr.started_at,
          'completed_at', tr.completed_at,
          'created_at', tr.created_at,
          'metadata', tr.metadata
        )
        ORDER BY tr.created_at DESC
      )
      FROM agents.team_runs tr
      WHERE tr.ai_lenser_id = v_ai_lenser_id
      LIMIT 20
    ), '[]'::jsonb),
    'profiles', jsonb_build_object(
      'personality', COALESCE((SELECT jsonb_agg(pp ORDER BY pp.updated_at DESC) FROM agents.personality_profiles pp WHERE pp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb),
      'memory', COALESCE((SELECT jsonb_agg(mp ORDER BY mp.updated_at DESC) FROM agents.memory_profiles mp WHERE mp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb),
      'tools', COALESCE((SELECT jsonb_agg(tp ORDER BY tp.updated_at DESC) FROM agents.tool_profiles tp WHERE tp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb),
      'models', COALESCE((SELECT jsonb_agg(modp ORDER BY modp.updated_at DESC) FROM agents.model_profiles modp WHERE modp.ai_lenser_id = v_ai_lenser_id), '[]'::jsonb)
    ),
    'workflow_assignments', COALESCE((
      SELECT jsonb_agg(wa ORDER BY wa.updated_at DESC)
      FROM agents.workflow_assignments wa
      WHERE wa.ai_lenser_id = v_ai_lenser_id
    ), '[]'::jsonb)
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "max_tokens_per_second" integer, "injected_delay_ms" integer, "max_context_tokens" integer, "allowed_model_tier" "text", "time_budget_ms" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT hp.id, hp.battle_id, hp.max_tokens_per_second, hp.injected_delay_ms,
         hp.max_context_tokens, hp.allowed_model_tier, hp.time_budget_ms
  FROM battles.ai_handicap_policies hp
  WHERE hp.battle_id = p_battle_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "contender_id" "uuid", "criterion_id" "uuid", "score" numeric, "rationale" "text", "model_key" "text", "run_id" "uuid", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT id, contender_id, criterion_id, score, rationale, model_key, run_id, created_at
  FROM battles.fn_get_ai_judge_verdicts(p_battle_id);
$$;


ALTER FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") RETURNS TABLE("id" "uuid", "key" "text", "name" "text", "provider_id" "uuid", "description" "text", "capabilities" "text"[], "temperature" numeric, "max_tokens" integer, "context_window_tokens" integer, "supports_tools" boolean, "supports_vision" boolean, "is_active" boolean, "input_modalities" "text"[], "output_modalities" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT
    m.id, m.key, m.name, m.provider_id, m.description,
    m.capabilities, m.temperature, m.max_tokens, m.context_window_tokens,
    m.supports_tools, m.supports_vision,
    m.is_active, m.input_modalities, m.output_modalities,
    m.created_at
  FROM ai.models m
  WHERE m.id = p_model_id;
$$;


ALTER FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") IS 'Security wrapper: fetch an AI model row by ID.';



CREATE OR REPLACE FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") RETURNS TABLE("id" "uuid", "key" "text", "display_name" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
  SELECT p.id, p.key, p.display_name
  FROM ai.providers p
  WHERE p.id = p_provider_id;
$$;


ALTER FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") IS 'Security wrapper: fetch AI provider id, key, and display_name by provider UUID.';



CREATE OR REPLACE FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ar.*)
  FROM agents.approval_requests_v ar
  WHERE ar.request_id = p_request_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = ar.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_auth_profile_gate"() RETURNS TABLE("status" "text", "deletion_requested_at" timestamp with time zone, "deletion_deadline_at" timestamp with time zone, "onboarding_step" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT p.status, p.deletion_requested_at, p.deletion_deadline_at, p.onboarding_step
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_auth_profile_gate"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_auth_profile_gate"() IS 'Security wrapper: return status/deletion/onboarding gate fields for the current user''s profile.';



CREATE OR REPLACE FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) RETURNS TABLE("entity_id" "uuid", "lenser_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT r.entity_id, r.lenser_id, r.reaction, r.created_at
  FROM content.reactions r
  WHERE r.entity_type::text = p_entity_type
    AND r.entity_id   = ANY(p_entity_ids);
$$;


ALTER FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) IS 'Security wrapper: return all reaction rows for an array of entity IDs (same type).';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_slug" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "finalized_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone, "task_source" "text", "contender_structure" "text", "judging_mode" "text", "challenge_type" "text", "shared_input_snapshot" "jsonb", "lenser_battle_policy" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT
    b.id, b.slug, b.title, b.task_prompt, b.status, b.total_vote_count,
    b.published_at, b.voting_opens_at, b.voting_closes_at, b.finalized_at,
    b.battle_type::text, b.voter_eligibility::text, b.handicap_config,
    b.creator_lenser_id, b.forum_thread_id,
    b.workflow_id, b.lens_id, b.execution_starts_at,
    b.auto_publish, b.voting_duration_hours, b.vote_velocity, b.og_image_url,
    b.winner_contender_id, b.parent_battle_id, b.deleted_at,
    -- V2 axes
    b.task_source,
    b.contender_structure::text,
    b.judging_mode::text,
    b.challenge_type,
    b.shared_input_snapshot,
    b.lenser_battle_policy
  FROM battles.battles b
  WHERE (p_battle_id IS NULL OR b.id   = p_battle_id)
    AND (p_slug      IS NULL OR b.slug = p_slug)
    AND b.deleted_at IS NULL
    AND (
      b.status <> 'draft'
      OR b.creator_lenser_id = lensers.get_auth_lenser_id()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") IS 'Security wrapper: look up a battle by id or slug. Public battles are visible to everyone; drafts only to the creator. Returns NULL when not found or draft-and-not-creator. Now includes V2 fields: task_source, contender_structure, judging_mode, challenge_type, shared_input_snapshot, lenser_battle_policy.';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "finalized_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone, "task_source" "text", "contender_structure" "text", "judging_mode" "text", "challenge_type" "text", "shared_input_snapshot" "jsonb", "lenser_battle_policy" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT * FROM public.fn_get_battle(NULL::"uuid", p_slug);
$$;


ALTER FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") IS 'Alias for fn_get_battle(p_slug). Return type kept in sync with fn_get_battle V2 (Phase CS V2 fields: task_source, contender_structure, judging_mode, challenge_type, shared_input_snapshot, lenser_battle_policy).';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "battle_id" "uuid", "lenser_id" "uuid", "body" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "lenser_handle" "text", "lenser_display_name" "text", "lenser_avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
  SELECT
    c.id,
    c.battle_id,
    c.lenser_id,
    c.body,
    c.created_at,
    c.updated_at,
    p.handle          AS lenser_handle,
    p.display_name    AS lenser_display_name,
    p.avatar_url      AS lenser_avatar_url
  FROM battles.comments  c
  JOIN lensers.profiles  p ON p.id = c.lenser_id
  WHERE c.battle_id = p_battle_id
  ORDER BY c.created_at ASC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer DEFAULT 50, "p_before_ts" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_before_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "lenser_id" "uuid", "body" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "lenser_handle" "text", "lenser_display_name" "text", "lenser_avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
  SELECT
    c.id,
    c.battle_id,
    c.lenser_id,
    c.body,
    c.created_at,
    c.updated_at,
    p.handle          AS lenser_handle,
    p.display_name    AS lenser_display_name,
    p.avatar_url      AS lenser_avatar_url
  FROM battles.comments c
  JOIN lensers.profiles p ON p.id = c.lenser_id
  WHERE c.battle_id = p_battle_id
    AND (
      p_before_ts IS NULL
      OR (c.created_at, c.id) < (p_before_ts, p_before_id)
    )
  ORDER BY c.created_at DESC, c.id DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "slot" "text", "contender_type" "text", "display_name" "text", "contender_ref_id" "uuid", "contender_status" "text", "entry_mode" "text", "joined_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT c.id, c.battle_id, c.slot::text, c.contender_type::text, c.display_name,
         c.contender_ref_id, c.contender_status, c.entry_mode,
         c.joined_at
  FROM battles.contenders c
  WHERE c.battle_id = p_battle_id
  ORDER BY c.slot;
$$;


ALTER FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") RETURNS TABLE("battle_id" "uuid", "winner_lenser_id" "uuid", "loser_lenser_id" "uuid", "winner_score_before" numeric, "winner_score_after" numeric, "loser_score_before" numeric, "loser_score_after" numeric, "is_draw" boolean, "k_factor" integer, "computed_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT
    el.battle_id, el.winner_lenser_id, el.loser_lenser_id,
    el.winner_score_before, el.winner_score_after,
    el.loser_score_before,  el.loser_score_after,
    el.is_draw, el.k_factor, el.computed_at
  FROM reputation.elo_battle_log el
  WHERE el.battle_id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") IS 'Security wrapper: return ELO score change records for a specific battle.';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT to_jsonb(ec.*)
  FROM battles.execution_configs ec
  WHERE ec.battle_id = p_battle_id
    AND EXISTS (
      SELECT 1 FROM battles.battles b
      WHERE b.id = ec.battle_id
        AND (b.creator_lenser_id = lensers.get_auth_lenser_id()
             OR b.status::text IN ('published', 'closed'))
    );
$$;


ALTER FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "status" "text", "worker_id" "text", "claimed_at" timestamp with time zone, "completed_at" timestamp with time zone, "retry_count" integer, "max_retries" integer, "error_message" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT j.id, j.battle_id, j.contender_id, j.slot, j.status, j.worker_id,
         j.claimed_at, j.completed_at, j.retry_count, j.max_retries,
         j.error_message, j.created_at
  FROM battles.battle_execution_jobs j
  JOIN battles.battles b ON b.id = j.battle_id
  WHERE j.battle_id = p_battle_id
    AND (b.creator_lenser_id = lensers.get_auth_lenser_id() OR public.is_admin())
  ORDER BY j.slot;
$$;


ALTER FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_full"("p_slug" "text") RETURNS "battles"."v_battle_full"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT *
  FROM battles.v_battle_full
  WHERE slug = p_slug
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_battle_full"("p_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") IS 'Returns full battle state for BattleDetailPage by slug. Single roundtrip covers contenders, submissions, vote aggregates, entity map, and rule snapshot hash via v_battle_full view.';



CREATE OR REPLACE FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "slot" "text", "status" "text", "claimed_at" timestamp with time zone, "completed_at" timestamp with time zone, "retry_count" integer, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT id, battle_id, slot, status, claimed_at, completed_at, retry_count, created_at
  FROM battles.v_execution_jobs_public
  WHERE battle_id = p_battle_id
  ORDER BY slot;
$$;


ALTER FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_battle        battles.battles;
  v_battle_id     UUID;
  v_contenders    JSONB;
  v_submissions   JSONB;
  v_aggregates    JSONB;
  v_scorecards    JSONB;
  v_criteria      JSONB;
  v_verdicts      JSONB;
BEGIN
  -- Fetch battle and enforce status guard
  SELECT * INTO v_battle
  FROM battles.battles
  WHERE slug = p_battle_slug
    AND status IN ('published', 'closed');

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  v_battle_id := v_battle.id;

  SELECT COALESCE(jsonb_agg(row_to_json(c)), '[]')
  INTO v_contenders
  FROM (
    SELECT id, battle_id, slot, contender_type, display_name, lenser_id,
           contender_ref_id, status, invited_at, joined_at
    FROM battles.contenders
    WHERE battle_id = v_battle_id
    ORDER BY slot
  ) c;

  SELECT COALESCE(jsonb_agg(row_to_json(s)), '[]')
  INTO v_submissions
  FROM (
    SELECT id, battle_id, contender_id, content_text, content_url,
           submitted_at, word_count, char_count
    FROM battles.submissions
    WHERE battle_id = v_battle_id
  ) s;

  SELECT COALESCE(jsonb_agg(row_to_json(a)), '[]')
  INTO v_aggregates
  FROM (
    SELECT battle_id, contender_id, raw_vote_count, weighted_vote_sum,
           draw_count, rank_position
    FROM battles.vote_aggregates
    WHERE battle_id = v_battle_id
  ) a;

  SELECT COALESCE(jsonb_agg(row_to_json(sc)), '[]')
  INTO v_scorecards
  FROM (
    SELECT id, battle_id, contender_id, rubric_criterion_id, result, explanation
    FROM battles.scorecards
    WHERE battle_id = v_battle_id
  ) sc;

  SELECT COALESCE(jsonb_agg(row_to_json(cr)), '[]')
  INTO v_criteria
  FROM (
    SELECT id, title AS name, description, weight
    FROM battles.rubric_criteria
    WHERE id IN (
      SELECT rubric_criterion_id FROM battles.scorecards WHERE battle_id = v_battle_id
    )
  ) cr;

  SELECT COALESCE(jsonb_agg(row_to_json(v)), '[]')
  INTO v_verdicts
  FROM (
    SELECT id, contender_id, criterion_id, score, rationale, model_key, run_id, created_at
    FROM battles.ai_judge_verdicts
    WHERE battle_id = v_battle_id
  ) v;

  RETURN jsonb_build_object(
    'battle',       row_to_json(v_battle),
    'contenders',   v_contenders,
    'submissions',  v_submissions,
    'aggregates',   v_aggregates,
    'scorecards',   v_scorecards,
    'criteria',     v_criteria,
    'verdicts',     v_verdicts
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "rubric_criterion_id" "uuid", "result" "text", "explanation" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT sc.id, sc.battle_id, sc.contender_id, sc.rubric_criterion_id,
         sc.result, sc.explanation
  FROM battles.scorecards sc
  WHERE sc.battle_id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") RETURNS TABLE("battle_id" "uuid", "slug" "text", "title" "text", "status" "text", "total_vote_count" integer, "finalized_at" timestamp with time zone, "winner_contender_id" "uuid", "deleted_at" timestamp with time zone, "contender_a_id" "uuid", "contender_a_name" "text", "contender_b_id" "uuid", "contender_b_name" "text", "elo_winner_before" numeric, "elo_winner_after" numeric, "elo_loser_before" numeric, "elo_loser_after" numeric, "elo_is_draw" boolean)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'reputation'
    AS $$
DECLARE
  v_battle_id uuid;
  v_status    text;
  v_deleted   timestamptz;
BEGIN
  SELECT b.id, b.status, b.deleted_at
  INTO v_battle_id, v_status, v_deleted
  FROM battles.battles b
  WHERE b.slug = p_slug
  LIMIT 1;

  IF NOT FOUND OR v_deleted IS NOT NULL OR v_status = 'draft' THEN
    RETURN;
  END IF;

  RETURN QUERY
  SELECT
    b.id, b.slug, b.title, b.status, b.total_vote_count, b.finalized_at,
    b.winner_contender_id, b.deleted_at,
    a_c.id, a_c.display_name,
    b_c.id, b_c.display_name,
    el.winner_score_before, el.winner_score_after,
    el.loser_score_before, el.loser_score_after,
    el.is_draw
  FROM battles.battles b
  LEFT JOIN battles.contenders a_c ON a_c.battle_id = b.id AND a_c.slot = 'A'
  LEFT JOIN battles.contenders b_c ON b_c.battle_id = b.id AND b_c.slot = 'B'
  LEFT JOIN reputation.elo_battle_log el ON el.battle_id = b.id
  WHERE b.id = v_battle_id
  LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "content_text" "text", "content_url" "text", "status" "text", "submitted_at" timestamp with time zone, "execution_run_id" "uuid", "is_final" boolean, "media_url" "text", "mime_type" "text", "output_modality" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT s.id, s.battle_id, s.contender_id, s.content_text, s.content_url,
         s.status::text, s.submitted_at, s.execution_run_id, s.is_final,
         s.media_url, s.mime_type, s.output_modality
  FROM battles.submissions s
  JOIN battles.battles b ON b.id = s.battle_id
  WHERE s.battle_id = p_battle_id
    AND b.deleted_at IS NULL
    AND (
      b.status::text = 'published'   -- anon: only published results are public
      OR auth.uid() IS NOT NULL      -- authenticated: any non-deleted battle is accessible
    );
$$;


ALTER FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_battles_feed"("p_status" "text" DEFAULT NULL::"text", "p_battle_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "published_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "total_vote_count" integer, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "contender_a_id" "uuid", "contender_a_name" "text", "contender_a_type" "text", "contender_b_id" "uuid", "contender_b_name" "text", "contender_b_type" "text", "winner_slot" "text", "content_type" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    v.id,
    v.slug,
    v.title,
    v.status::text,
    v.published_at,
    v.battle_type::text,
    v.voter_eligibility::text,
    v.total_vote_count,
    v.voting_opens_at,
    v.voting_closes_at,
    v.contender_a_id,
    v.contender_a_name,
    v.contender_a_type::text,
    v.contender_b_id,
    v.contender_b_name,
    v.contender_b_type::text,
    v.winner_slot,
    v.content_type
  FROM battles.v_battle_feed_item v
  WHERE
    -- Defence-in-depth: always exclude draft/open regardless of p_status parameter
    v.status::text IN ('voting', 'scoring', 'closed', 'published', 'archived')
    AND (p_status      IS NULL OR v.status::text      = p_status)
    AND (p_battle_type IS NULL OR v.battle_type::text = p_battle_type)
    AND (p_cursor      IS NULL OR v.published_at      < p_cursor)
  ORDER BY v.published_at DESC NULLS LAST
  LIMIT LEAST(COALESCE(p_limit, 20), 100);
$$;


ALTER FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Paginated battle feed. Filters by status and battle_type (both optional text). Always excludes draft/open battles. Keyset pagination via p_cursor (published_at of last item). LIMIT capped at 100. Returns content_type for client-side category badges. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "category" "text", "elo_rating" numeric, "uncertainty" numeric, "battles_played" integer, "wins" integer, "draws" integer, "losses" integer, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT cr.id, cr.lenser_id, cr.category, cr.elo_rating, cr.uncertainty,
         cr.battles_played, cr.wins, cr.draws, cr.losses, cr.updated_at
  FROM reputation.contender_ratings cr
  WHERE cr.lenser_id = p_lenser_id
    AND (p_category IS NULL OR cr.category = p_category)
  ORDER BY cr.updated_at DESC
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") IS 'Security wrapper: fetch the most recent ELO contender rating for a lenser, optionally filtered to a specific category.';



CREATE OR REPLACE FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT to_jsonb(r.*)
  FROM public.fn_get_contender_rating(p_lenser_id) r
  LIMIT LEAST(GREATEST(p_limit, 1), 100);
$$;


ALTER FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer DEFAULT 30) RETURNS TABLE("day" "date", "battles" bigint, "wins" bigint, "votes_received" bigint, "xp_earned" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'xp', 'public'
    AS $$
  WITH date_series AS (
    SELECT generate_series(
      CURRENT_DATE - (p_days - 1) * INTERVAL '1 day',
      CURRENT_DATE,
      INTERVAL '1 day'
    )::date AS day
  ),
  battle_stats AS (
    SELECT
      b.published_at::date       AS day,
      COUNT(*)                   AS battles,
      COUNT(*) FILTER (
        WHERE b.winner_contender_id = c.id
      )                          AS wins
    FROM battles.contenders c
    JOIN battles.battles b ON b.id = c.battle_id
    WHERE c.contender_ref_id = p_lenser_id
      AND b.status IN ('closed', 'published')
      AND b.published_at >= CURRENT_DATE - p_days * INTERVAL '1 day'
    GROUP BY 1
  ),
  vote_stats AS (
    SELECT
      v.created_at::date AS day,
      COUNT(*)           AS votes_received
    FROM battles.votes v
    JOIN battles.contenders c ON c.id = v.voted_contender_id
    WHERE c.contender_ref_id = p_lenser_id
      AND v.created_at >= CURRENT_DATE - p_days * INTERVAL '1 day'
    GROUP BY 1
  ),
  xp_stats AS (
    SELECT
      x.created_at::date AS day,
      COALESCE(SUM(x.xp), 0) AS xp_earned
    FROM xp.events x
    WHERE x.lenser_id = p_lenser_id
      AND x.created_at >= CURRENT_DATE - p_days * INTERVAL '1 day'
    GROUP BY 1
  )
  SELECT
    ds.day,
    COALESCE(bs.battles, 0)        AS battles,
    COALESCE(bs.wins, 0)           AS wins,
    COALESCE(vs.votes_received, 0) AS votes_received,
    COALESCE(xs.xp_earned, 0)      AS xp_earned
  FROM date_series ds
  LEFT JOIN battle_stats bs ON bs.day = ds.day
  LEFT JOIN vote_stats    vs ON vs.day = ds.day
  LEFT JOIN xp_stats      xs ON xs.day = ds.day
  ORDER BY ds.day ASC
$$;


ALTER FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) IS 'Returns a daily timeseries of battles, wins, votes received, and XP earned for a given lenser over the last p_days days. SECURITY DEFINER — no RLS bypass needed.';



CREATE OR REPLACE FUNCTION "public"."fn_get_dlq_counts"() RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lenses'
    AS $$
  SELECT jsonb_build_object(
    'battle_dlq_count',
    (SELECT COUNT(*) FROM battles.battle_execution_dead_letters WHERE resolved_at IS NULL),
    'workflow_dlq_count',
    (SELECT COUNT(*) FROM lenses.workflow_run_dead_letters WHERE resolved_at IS NULL)
  );
$$;


ALTER FUNCTION "public"."fn_get_dlq_counts"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_unresolved_only" boolean DEFAULT false, "p_limit" integer DEFAULT 50) RETURNS TABLE("id" "uuid", "job_id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "error_code" "text", "error_message" "text", "attempt_count" integer, "payload" "jsonb", "resolved_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT dl.id, dl.job_id, dl.battle_id, dl.contender_id, dl.slot,
         dl.error_code, dl.error_message, dl.attempt_count, dl.payload,
         dl.resolved_at, dl.created_at
  FROM battles.battle_execution_dead_letters dl
  WHERE (p_battle_id IS NULL       OR dl.battle_id = p_battle_id)
    AND (NOT p_unresolved_only     OR dl.resolved_at IS NULL)
  ORDER BY dl.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("attachment_id" "uuid", "object_id" "uuid", "entity_type" "text", "entity_id" "uuid", "binding_key" "text", "attached_at" timestamp with time zone, "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'media'
    AS $$
  SELECT
    a.id           AS attachment_id,
    a.object_id,
    a.entity_type,
    a.entity_id,
    a.binding_key,
    a.attached_at,
    o.bucket, o.object_key, o.external_url, o.mime_type, o.media_type,
    o.name, o.byte_size, o.visibility, o.lifecycle_state, o.metadata
  FROM media.attachments a
  JOIN media.objects o ON o.id = a.object_id
  WHERE a.entity_type = p_entity_type
    AND a.entity_id   = p_entity_id;
$$;


ALTER FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: list media attachments for an entity with nested object metadata.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT COALESCE(
    jsonb_object_agg(r.reaction, r.cnt),
    '{}'::jsonb
  )
  FROM (
    SELECT reaction, COUNT(*) AS cnt
    FROM content.reactions
    WHERE entity_type::text = p_entity_type
      AND entity_id   = p_entity_id
    GROUP BY reaction
  ) r;
$$;


ALTER FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: return a jsonb map of reaction → count for a single entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("reaction" "text", "reacted" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT
    rt.reaction::text,
    EXISTS (
      SELECT 1
      FROM content.reactions r
      WHERE r.entity_type::text = p_entity_type
        AND r.entity_id         = p_entity_id
        AND r.lenser_id         = lensers.get_auth_lenser_id()
        AND r.reaction          = rt.reaction
    ) AS reacted
  FROM unnest(enum_range(NULL::content.reaction_enum)) rt(reaction);
$$;


ALTER FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Returns one row per reaction type with reacted=true if the current user reacted. Used by reactionRepository.ts getReactions(). Requires authentication.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid" DEFAULT NULL::"uuid", "p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "entity_type" "text", "entity_id" "uuid", "lenser_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT
    r.id,
    r.entity_type::text,
    r.entity_id,
    r.lenser_id,
    r.reaction::text,
    r.created_at
  FROM content.reactions r
  WHERE r.entity_type::text = p_entity_type
    AND (p_entity_id IS NULL OR r.entity_id = p_entity_id)
    AND (p_lenser_id IS NULL OR r.lenser_id = p_lenser_id);
$$;


ALTER FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") IS 'Flexible reaction query: filter by entity_type (required), entity_id (optional), and/or lenser_id (optional). Used by reactionRepository getReactionsFor, getBatchUserReactions, and getLenserHistory.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("tag_id" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT tm.tag_id
  FROM content.tag_map tm
  WHERE tm.entity_type::text = p_entity_type
    AND tm.entity_id   = p_entity_id;
$$;


ALTER FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: return tag UUIDs mapped to an entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("title" "text", "description" "text", "content" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content'
    AS $$
  SELECT et.title, et.description, et.content
  FROM content.entity_translations et
  WHERE et.entity_type::text = p_entity_type
    AND et.entity_id   = p_entity_id
    AND et.is_original = true
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: fetch the original (is_original=true) translation for any entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(eb.*)
  FROM agents.evaluation_baselines eb
  JOIN agents.evaluations ev ON ev.id = eb.evaluation_id
  WHERE eb.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(erv.*)
  FROM agents.evaluation_results_v erv
  JOIN agents.evaluation_runs er ON er.id = erv.run_id
  JOIN agents.evaluations ev ON ev.id = er.evaluation_id
  WHERE erv.run_id = p_run_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin());
$$;


ALTER FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "run_id" "uuid", "artifact_kind" "text", "content_text" "text", "content_json" "jsonb", "visibility" "text", "is_primary_output" boolean, "output_type" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers'
    AS $$
  SELECT
    a.id, a.run_id, a.artifact_kind, a.content_text, a.content_json,
    a.visibility, a.is_primary_output, a.output_type, a.created_at
  FROM execution.artifacts a
  WHERE a.run_id = p_run_id
    AND EXISTS (
      SELECT 1 FROM execution.fn_get_run_details(p_run_id) LIMIT 1
    )
  ORDER BY a.is_primary_output DESC, a.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") IS 'Security wrapper: list artifacts for a run. Access gated via execution.fn_get_run_details — returns empty if caller cannot see the run.';



CREATE OR REPLACE FUNCTION "public"."fn_get_execution_status"() RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'admin', 'platform', 'lenses', 'battles', 'public'
    AS $$
  SELECT jsonb_build_object(
    'system_kill_switch_active',
      public.fn_kill_switch_active('system'),
    'queue_frozen',
      (SELECT queue_frozen  FROM admin.execution_control WHERE id = 1),
    'frozen_reason',
      (SELECT frozen_reason FROM admin.execution_control WHERE id = 1),
    'active_run_count',
      (SELECT COUNT(*) FROM lenses.workflow_runs
       WHERE status IN ('running', 'streaming', 'recovered')),
    'queued_run_count',
      (SELECT COUNT(*) FROM lenses.workflow_runs
       WHERE status IN ('queued', 'pending')),
    'active_battle_job_count',
      (SELECT COUNT(*) FROM battles.battle_execution_jobs
       WHERE status IN ('claimed', 'running')),
    'queued_battle_job_count',
      (SELECT COUNT(*) FROM battles.battle_execution_jobs
       WHERE status = 'queued'),
    'active_worker_count',
      (SELECT COUNT(*) FROM platform.api_worker_heartbeats
       WHERE last_seen_at >= now() - interval '2 minutes'),
    'stale_worker_count',
      (SELECT COUNT(*) FROM platform.api_worker_heartbeats
       WHERE last_seen_at < now() - interval '2 minutes'),
    'dlq_workflow_count',
      (SELECT COUNT(*) FROM lenses.workflow_run_dead_letters
       WHERE resolved_at IS NULL),
    'dlq_battle_count',
      (SELECT COUNT(*) FROM battles.battle_execution_dead_letters
       WHERE resolved_at IS NULL)
  );
$$;


ALTER FUNCTION "public"."fn_get_execution_status"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_execution_status"() IS 'Global execution health dashboard. Returns system_kill_switch_active, queue_frozen, active/queued run/job counts, worker health, and DLQ depths. Granted to authenticated (not admin-only) so frontend banners work for all users.';



CREATE OR REPLACE FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(fo.*)
  FROM agents.v_human_fleet_overview fo
  WHERE fo.human_lenser_id = p_human_lenser_id
    AND (p_human_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") RETURNS "text"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT COALESCE(
    (SELECT
       CASE r.status
         WHEN 'pending'  THEN 'pending'
         WHEN 'accepted' THEN 'following'
       END
     FROM lensers.relationships r
     JOIN lensers.profiles src ON src.id = r.source_profile_id AND src.user_id = auth.uid()
     WHERE r.target_profile_id = p_target_profile_id
       AND r.removed_at IS NULL
     LIMIT 1),
    'none'
  )
$$;


ALTER FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_gateway_device_health"() RETURNS TABLE("device_id" "uuid", "hostname" "text", "daemon_version" "text", "last_seen_at" timestamp with time zone, "approved_at" timestamp with time zone, "revoked_at" timestamp with time zone, "kill_switch" boolean, "created_at" timestamp with time zone, "pending_commands" bigint, "unacked_commands" bigint)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
DECLARE
  v_uid UUID := auth.uid();
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
    SELECT v.device_id, v.hostname, v.daemon_version, v.last_seen_at,
           v.approved_at, v.revoked_at, v.kill_switch, v.created_at,
           v.pending_commands, v.unacked_commands
      FROM agents.v_gateway_device_health v
     WHERE v.owner_id = v_uid
     ORDER BY v.last_seen_at DESC NULLS LAST,
              v.created_at DESC;
END $$;


ALTER FUNCTION "public"."fn_get_gateway_device_health"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer DEFAULT 50) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_wins" bigint, "total_battles" bigint, "win_rate" numeric, "total_votes_received" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers', 'reputation'
    AS $$
  SELECT
    lp.id                                                                  AS lenser_id,
    lp.handle,
    lp.display_name,
    lp.avatar_url,
    COUNT(c.id) FILTER (WHERE b.winner_contender_id = c.id)               AS total_wins,
    COUNT(c.id)                                                            AS total_battles,
    CASE WHEN COUNT(c.id) > 0
         THEN ROUND(
           COUNT(c.id) FILTER (WHERE b.winner_contender_id = c.id)::numeric
           / COUNT(c.id)::numeric, 4)
         ELSE 0::numeric
    END                                                                    AS win_rate,
    COALESCE(SUM(va.raw_vote_count), 0)                                    AS total_votes_received
  FROM lensers.profiles lp
  JOIN battles.contenders c ON c.contender_ref_id = lp.id
  JOIN battles.battles b
    ON b.id = c.battle_id AND b.status::text IN ('published', 'closed')
  LEFT JOIN battles.vote_aggregates va ON va.contender_id = c.id
  GROUP BY lp.id, lp.handle, lp.display_name, lp.avatar_url
  HAVING COUNT(c.id) > 0
  ORDER BY total_wins DESC, total_battles DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer DEFAULT 50, "p_before_ts" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_before_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "sender_id" "uuid", "sender_handle" "text", "sender_role" "text", "body" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    id,
    battle_id,
    sender_id,
    sender_handle,
    sender_role,
    body,
    created_at
  FROM battles.global_messages
  WHERE battle_id = p_battle_id
    AND (
      p_before_ts IS NULL
      OR (created_at, id) < (p_before_ts, p_before_id)
    )
  ORDER BY created_at DESC, id DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") RETURNS TABLE("total_battles" bigint, "a_wins" bigint, "b_wins" bigint, "draws" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  WITH shared_battles AS (
    -- Battles where both lensers participated
    SELECT ca.battle_id
    FROM battles.contenders ca
    JOIN battles.contenders cb ON cb.battle_id = ca.battle_id
      AND cb.contender_ref_id = p_lenser_b
    WHERE ca.contender_ref_id = p_lenser_a
  ),
  outcomes AS (
    SELECT
      b.id AS battle_id,
      MAX(CASE WHEN c.contender_ref_id = p_lenser_a AND b.winner_contender_id = c.id THEN 1 ELSE 0 END) AS a_won,
      MAX(CASE WHEN c.contender_ref_id = p_lenser_b AND b.winner_contender_id = c.id THEN 1 ELSE 0 END) AS b_won
    FROM shared_battles sb
    JOIN battles.battles b ON b.id = sb.battle_id
    JOIN battles.contenders c ON c.battle_id = b.id
      AND c.contender_ref_id IN (p_lenser_a, p_lenser_b)
    WHERE b.status IN ('closed', 'published')
    GROUP BY b.id
  )
  SELECT
    COUNT(*)                               AS total_battles,
    SUM(CASE WHEN a_won = 1 AND b_won = 0 THEN 1 ELSE 0 END) AS a_wins,
    SUM(CASE WHEN b_won = 1 AND a_won = 0 THEN 1 ELSE 0 END) AS b_wins,
    SUM(CASE WHEN a_won = b_won           THEN 1 ELSE 0 END) AS draws
  FROM outcomes
$$;


ALTER FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") IS 'Returns aggregate win/loss/draw stats for two lensers across all battles where both participated. SECURITY DEFINER — caller visibility is public.';



CREATE OR REPLACE FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("occurred_at" timestamp with time zone, "kind" "text", "ai_lenser_id" "uuid", "ai_lenser_handle" "text", "ai_lenser_name" "text", "title" "text", "status" "text", "team_run_id" "uuid", "workflow_id" "uuid", "schedule_id" "uuid", "action_type" "text", "payload" "jsonb")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_caller uuid;
  v_safe_limit integer := COALESCE(p_limit, 50);
  v_safe_offset integer := COALESCE(p_offset, 0);
BEGIN
  v_caller := lensers.get_auth_human_lenser_id();
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  IF v_caller <> p_human_lenser_id THEN
    RAISE EXCEPTION 'Forbidden: cannot read another human Lenser''s activity feed.'
      USING ERRCODE = '42501';
  END IF;

  IF v_safe_limit < 1 THEN v_safe_limit := 1; END IF;
  IF v_safe_limit > 200 THEN v_safe_limit := 200; END IF;
  IF v_safe_offset < 0 THEN v_safe_offset := 0; END IF;

  RETURN QUERY
  WITH owned_agents AS (
    SELECT o.ai_lenser_id, p.handle, p.display_name
    FROM agents.ownerships o
    JOIN agents.ai_lensers al ON al.id = o.ai_lenser_id
    JOIN lensers.profiles p ON p.id = al.profile_id
    WHERE o.owner_lenser_id = v_caller
      AND o.role IN ('owner', 'co_owner')
      AND o.revoked_at IS NULL
  ),
  approval_items AS (
    SELECT
      tr.created_at AS occurred_at,
      'approval_pending'::text AS kind,
      tr.ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      COALESCE(w.title, 'Untitled workflow') AS title,
      'pending'::text AS status,
      tr.id AS team_run_id,
      tr.workflow_id,
      NULL::uuid AS schedule_id,
      NULL::text AS action_type,
      jsonb_build_object(
        'gate_kind', tr.metadata->>'gate_kind',
        'requested_action', tr.metadata->>'requested_action',
        'requester_agent_id', tr.metadata->>'requester_agent_id'
      ) AS payload
    FROM agents.team_runs tr
    JOIN owned_agents oa ON oa.ai_lenser_id = tr.ai_lenser_id
    LEFT JOIN lenses.workflows w ON w.id = tr.workflow_id
    WHERE tr.approval_status = 'pending'
  ),
  run_items AS (
    SELECT
      COALESCE(tr.completed_at, tr.started_at, tr.created_at) AS occurred_at,
      'team_run'::text AS kind,
      tr.ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      COALESCE(w.title, 'Untitled workflow') AS title,
      tr.status,
      tr.id AS team_run_id,
      tr.workflow_id,
      NULL::uuid AS schedule_id,
      NULL::text AS action_type,
      jsonb_build_object(
        'approval_status', tr.approval_status,
        'started_at', tr.started_at,
        'completed_at', tr.completed_at
      ) AS payload
    FROM agents.team_runs tr
    JOIN owned_agents oa ON oa.ai_lenser_id = tr.ai_lenser_id
    LEFT JOIN lenses.workflows w ON w.id = tr.workflow_id
    WHERE tr.approval_status <> 'pending'
  ),
  schedule_items AS (
    SELECT
      COALESCE(s.last_run_at, s.created_at) AS occurred_at,
      'schedule_dispatch'::text AS kind,
      al.id AS ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      COALESCE(w.title, 'Untitled workflow') AS title,
      COALESCE(s.last_dispatch_status, 'never_dispatched') AS status,
      NULL::uuid AS team_run_id,
      s.workflow_id,
      s.id AS schedule_id,
      NULL::text AS action_type,
      jsonb_build_object(
        'cron_expr', s.cron_expr,
        'timezone', s.timezone,
        'next_run_at', s.next_run_at,
        'last_error_message', s.last_error_message,
        'assignee_type', s.assignee_type,
        'assignee_id', s.assignee_id
      ) AS payload
    FROM lenses.workflow_schedules s
    JOIN lenses.workflows w ON w.id = s.workflow_id
    -- Schedules are bound to a workflow whose lenser_id is the AI workspace
    -- profile id when assignee_type='agent'; otherwise we resolve via the
    -- assignee_id column.
    JOIN agents.ai_lensers al
      ON al.id = COALESCE(
           CASE WHEN s.assignee_type = 'agent' THEN s.assignee_id END,
           (SELECT id FROM agents.ai_lensers ail WHERE ail.profile_id = w.lenser_id)
         )
    JOIN owned_agents oa ON oa.ai_lenser_id = al.id
  ),
  action_items AS (
    SELECT
      a.occurred_at,
      'agent_action'::text AS kind,
      a.ai_lenser_id,
      oa.handle AS ai_lenser_handle,
      oa.display_name AS ai_lenser_name,
      a.action_type AS title,
      a.result AS status,
      NULL::uuid AS team_run_id,
      NULL::uuid AS workflow_id,
      NULL::uuid AS schedule_id,
      a.action_type,
      a.metadata AS payload
    FROM agents.action_logs a
    JOIN owned_agents oa ON oa.ai_lenser_id = a.ai_lenser_id
  ),
  combined AS (
    SELECT * FROM approval_items
    UNION ALL
    SELECT * FROM run_items
    UNION ALL
    SELECT * FROM schedule_items
    UNION ALL
    SELECT * FROM action_items
  )
  SELECT *
  FROM combined
  ORDER BY occurred_at DESC NULLS LAST
  LIMIT v_safe_limit OFFSET v_safe_offset;
END;
$$;


ALTER FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Cross-agent activity feed for the calling human Lenser. Aggregates pending approvals, recent team runs, schedule dispatches, and agent actions across every owned AI lenser. Caller must equal p_human_lenser_id.';



CREATE OR REPLACE FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "calibration_score" numeric, "total_judgments" integer, "agreement_rate" numeric, "kappa_score" numeric, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT jc.id, jc.lenser_id, jc.calibration_score, jc.total_judgments,
         jc.agreement_rate, jc.kappa_score, jc.updated_at
  FROM reputation.judge_calibrations jc
  WHERE jc.lenser_id = p_lenser_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") IS 'Security wrapper: return the judge calibration record for a lenser.';



CREATE OR REPLACE FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT to_jsonb(b.*)
  FROM battles.battles b
  WHERE b.workflow_id = p_workflow_id
    AND b.status::text = 'draft'
    AND b.creator_lenser_id = lensers.get_auth_lenser_id()
  ORDER BY b.created_at DESC
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text" DEFAULT 'xp'::"text", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS SETOF "xp"."v_leaderboard"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'reputation', 'lensers', 'public'
    AS $$
BEGIN
  IF p_order_by = 'elo' THEN
    RETURN QUERY
      SELECT * FROM xp.v_leaderboard
      ORDER BY elo_score DESC, total_xp DESC, lenser_id
      LIMIT  LEAST(COALESCE(p_limit, 50), 100)
      OFFSET GREATEST(COALESCE(p_offset, 0), 0);
  ELSE
    RETURN QUERY
      SELECT * FROM xp.v_leaderboard
      ORDER BY total_xp DESC, elo_score DESC, lenser_id
      LIMIT  LEAST(COALESCE(p_limit, 50), 100)
      OFFSET GREATEST(COALESCE(p_offset, 0), 0);
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) IS 'Paginated leaderboard for LeaderboardPage. p_order_by: ''xp'' (default) sorts by total_xp, ''elo'' sorts by elo_score. LIMIT capped at 100. Uses offset pagination (simple for leaderboard UX).';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") RETURNS TABLE("id" "uuid", "contender_id" "uuid", "battle_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "assigned_at" timestamp with time zone, "input_snapshot" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT la.id, la.contender_id, la.battle_id, la.lens_id, la.version_id,
         la.assigned_at, la.input_snapshot
  FROM battles.contender_lens_assignments la
  WHERE la.contender_id = p_contender_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
DECLARE
  v_viewer_id       uuid;
  v_lens            RECORD;
  v_translation     RECORD;
  v_author          RECORD;
  v_tags            jsonb;
  v_reaction_totals jsonb;
  v_user_reactions  jsonb;
  v_latest_ver      RECORD;
  v_params          jsonb;
BEGIN
  v_viewer_id := lensers.get_auth_lenser_id();

  -- Core lens: visibility check (owner sees own, everyone sees public)
  SELECT l.id, l.lenser_id, l.visibility::text, l.parent_lens_id,
         l.forked_from_execution_id, l.head_version_id,
         l.created_at, l.updated_at
  INTO v_lens
  FROM lenses.lenses l
  WHERE l.id = p_lens_id
    AND (
      l.visibility = 'public'::content.visibility_enum
      OR l.lenser_id = v_viewer_id
    );

  IF NOT FOUND THEN
    RETURN jsonb_build_object('error', 'not_found');
  END IF;

  -- Translation: title, description, content (prefers original, then 'en')
  SELECT et.title, et.description, et.content
  INTO v_translation
  FROM content.entity_translations et
  WHERE et.entity_type = 'lens'::content.entity_type_enum
    AND et.entity_id   = p_lens_id
  ORDER BY et.is_original DESC, (et.language_code = 'en') DESC, et.created_at DESC
  LIMIT 1;

  -- Author profile
  SELECT p.id, p.handle, p.display_name, p.avatar_url
  INTO v_author
  FROM lensers.profiles p
  WHERE p.id = v_lens.lenser_id;

  -- Tags via content.tag_map + content.tags + content.tag_translations (en fallback -> slug)
  SELECT COALESCE(
    jsonb_agg(jsonb_build_object(
      'id',   t.id,
      'slug', t.slug,
      'name', COALESCE(tt_en.name, t.slug)
    )),
    '[]'::jsonb
  )
  INTO v_tags
  FROM content.tag_map tm
  JOIN content.tags t ON t.id = tm.tag_id
  LEFT JOIN content.tag_translations tt_en
    ON tt_en.tag_id = t.id AND tt_en.language_code = 'en'
  WHERE tm.entity_type::text = 'lens'
    AND tm.entity_id          = p_lens_id;

  -- Reaction counts
  SELECT COALESCE(
    jsonb_object_agg(r.reaction::text, r.cnt),
    '{}'::jsonb
  )
  INTO v_reaction_totals
  FROM (
    SELECT reaction, COUNT(*) AS cnt
    FROM content.reactions
    WHERE entity_type::text = 'lens'
      AND entity_id          = p_lens_id
    GROUP BY reaction
  ) r;

  -- Current user's own reactions (empty if not authenticated)
  IF v_viewer_id IS NOT NULL THEN
    SELECT COALESCE(
      jsonb_object_agg(r.reaction::text, true),
      '{}'::jsonb
    )
    INTO v_user_reactions
    FROM content.reactions r
    WHERE r.entity_type::text = 'lens'
      AND r.entity_id          = p_lens_id
      AND r.lenser_id          = v_viewer_id;
  ELSE
    v_user_reactions := '{}'::jsonb;
  END IF;

  -- Latest published version
  SELECT v.id, v.version_number, v.template_body, v.status::text,
         v.changelog, v.published_at, v.created_at
  INTO v_latest_ver
  FROM lenses.versions v
  WHERE v.lens_id    = p_lens_id
    AND v.status     = 'published'::content.content_status
  ORDER BY v.version_number DESC
  LIMIT 1;

  -- Version parameters for latest published (empty array if no published version)
  IF v_latest_ver.id IS NOT NULL THEN
    SELECT COALESCE(
      jsonb_agg(
        jsonb_build_object(
          'id',        vp.id,
          'label',     vp.label,
          'toolId',    vp.tool_id,
          'versionId', vp.version_id,
          'tool',      jsonb_build_object(
            'id',               tk.id,
            'key',              tk.key,
            'label',            tk.label,
            'description',      tk.description,
            'category',         tk.category,
            'type',             tk.type,
            'required',         tk.required,
            'min_length',       tk.min_length,
            'max_length',       tk.max_length,
            'placeholder',      tk.placeholder,
            'help_text',        tk.help_text,
            'validation_schema',tk.validation_schema,
            'options',          tk.options,
            'sort_order',       tk.sort_order,
            'is_system',        tk.is_system,
            'icon',             tk.icon,
            'color',            tk.color
          )
        )
        ORDER BY tk.sort_order, vp.id
      ),
      '[]'::jsonb
    )
    INTO v_params
    FROM lenses.version_parameters vp
    JOIN lenses.tools tk ON tk.id = vp.tool_id
    WHERE vp.version_id = v_latest_ver.id;
  ELSE
    v_params := '[]'::jsonb;
  END IF;

  RETURN jsonb_build_object(
    'id',                        v_lens.id,
    'lenser_id',                 v_lens.lenser_id,
    'visibility',                v_lens.visibility,
    'parent_lens_id',            v_lens.parent_lens_id,
    'forked_from_execution_id',  v_lens.forked_from_execution_id,
    'head_version_id',           v_lens.head_version_id,
    'created_at',                v_lens.created_at,
    'updated_at',                v_lens.updated_at,
    'title',                     COALESCE(v_translation.title, 'Untitled'),
    'description',               v_translation.description,
    'content',                   COALESCE(v_translation.content, ''),
    'author_profile',            jsonb_build_object(
                                   'id',           v_author.id,
                                   'handle',       v_author.handle,
                                   'display_name', v_author.display_name,
                                   'avatar_url',   v_author.avatar_url
                                 ),
    'tags',                      COALESCE(v_tags, '[]'::jsonb),
    'reaction_totals',           v_reaction_totals,
    'user_reactions',            v_user_reactions,
    'latest_published_version',  CASE
      WHEN v_latest_ver.id IS NOT NULL THEN jsonb_build_object(
        'id',             v_latest_ver.id,
        'lens_id',        p_lens_id,
        'version_number', v_latest_ver.version_number,
        'template_body',  v_latest_ver.template_body,
        'status',         v_latest_ver.status,
        'changelog',      v_latest_ver.changelog,
        'published_at',   v_latest_ver.published_at,
        'created_at',     v_latest_ver.created_at,
        'parameters',     COALESCE(v_params, '[]'::jsonb)
      )
      ELSE NULL
    END
  );
END;
$$;


ALTER FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") IS 'Single-call bootstrap for LensDetailPage. Replaces 9 sequential RPCs with one roundtrip. Returns full lens detail + author + tags + reaction counts + user reactions + latest published version. STABLE = cacheable. Returns {"error":"not_found"} for inaccessible lenses.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("request_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "version_number" integer, "model_id" "uuid", "model_key" "text", "provider_key" "text", "funding_source" "text", "run_id" "uuid", "run_status" "text", "latency_ms" integer, "token_input" integer, "token_output" integer, "credit_cost" bigint, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lenses', 'ai', 'lensers', 'auth'
    AS $$
DECLARE
  v_limit  integer := LEAST(GREATEST(COALESCE(p_limit,  20), 1), 100);
  v_offset integer := GREATEST(COALESCE(p_offset, 0), 0);
BEGIN
  IF p_lens_id IS NULL THEN
    RETURN;
  END IF;

  RETURN QUERY
  SELECT *
  FROM execution.fn_get_lens_execution_history(p_lens_id, v_limit, v_offset);
END;
$$;


ALTER FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Public SECURITY DEFINER wrapper exposing execution.fn_get_lens_execution_history via PostgREST. Caps p_limit to 100, p_offset to >= 0. Returns empty set (not 404) for unknown lenses or unauthenticated callers — the inner function enforces auth.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") RETURNS TABLE("id" "uuid", "head_version_id" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT l.id, l.head_version_id
  FROM lenses.lenses l
  WHERE l.id = p_lens_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") IS 'Security wrapper: fetch a lens by ID and return its head_version_id. Used by the execution pipeline before queuing a run.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT COALESCE(
    jsonb_agg(to_jsonb(f.*) ORDER BY f.depth ASC),
    '[]'::jsonb
  )
  FROM lenses.vw_fork_history f
  WHERE f.lens_id = p_lens_id;
$$;


ALTER FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") IS 'Security wrapper: return the fork ancestry chain for a lens as a jsonb array ordered by depth ascending.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") RETURNS TABLE("id" "uuid", "lens_id" "uuid", "version_number" integer, "status" "text", "template_body" "text", "changelog" "text", "parent_version_id" "uuid", "published_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    v.id, v.lens_id, v.version_number, v.status::text,
    v.template_body, v.changelog, v.parent_version_id,
    v.published_at, v.created_at
  FROM lenses.versions v
  JOIN lenses.lenses l ON l.id = v.lens_id
  WHERE v.id = p_version_id
    AND (
      l.lenser_id = lensers.get_auth_lenser_id()
      OR (l.visibility = 'public' AND v.status = 'published')
    );
$$;


ALTER FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") IS 'Security wrapper: get a lens version. Owner sees any status; others see only published versions of public lenses.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT COALESCE(
    jsonb_agg(to_jsonb(vp.*) ORDER BY vp.label ASC),
    '[]'::jsonb
  )
  FROM lenses.version_parameters vp
  JOIN lenses.versions v   ON v.id = vp.version_id
  JOIN lenses.lenses   l   ON l.id = v.lens_id
  WHERE vp.version_id = p_version_id
    AND (
      l.lenser_id = lensers.get_auth_lenser_id()
      OR (l.visibility = 'public' AND v.status = 'published')
    );
$$;


ALTER FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") IS 'Security wrapper: return the parameter definitions for a lens version as a jsonb array ordered by label.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") RETURNS TABLE("date" "text", "count" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  WITH resolved AS (
    SELECT p.id AS lenser_id,
           COALESCE(pr.hide_actions, false) AS hide_actions
    FROM   lensers.profiles p
    LEFT JOIN lensers.preferences pr ON pr.lenser_id = p.id
    WHERE  p.handle = p_handle
    LIMIT  1
  ),
  calendar AS (
    SELECT (current_date - s.i)::date AS day
    FROM generate_series(0, 364) AS s(i)
  ),
  events AS (
    -- Lens creations
    SELECT date_trunc('day', l.created_at)::date AS day
    FROM   lenses.lenses l
    JOIN   resolved r ON r.lenser_id = l.lenser_id
    WHERE  l.visibility IN ('public', 'community')
      AND  l.status = 'published'
      AND  l.created_at >= (current_date - interval '364 days')

    UNION ALL

    -- Thread creations
    SELECT date_trunc('day', t.created_at)::date AS day
    FROM   content.threads t
    JOIN   resolved r ON r.lenser_id = t.lenser_id
    WHERE  t.visibility IN ('public', 'community')
      AND  t.status = 'published'
      AND  t.created_at >= (current_date - interval '364 days')

    UNION ALL

    -- Reactions (skipped when hide_actions = true via JOIN predicate)
    SELECT date_trunc('day', rx.created_at)::date AS day
    FROM   content.reactions rx
    JOIN   resolved r ON r.lenser_id = rx.lenser_id
                      AND r.hide_actions = false
    WHERE  rx.created_at >= (current_date - interval '364 days')
  ),
  daily_counts AS (
    SELECT day, count(*) AS cnt
    FROM   events
    GROUP BY day
  )
  SELECT to_char(c.day, 'YYYY-MM-DD') AS date,
         COALESCE(dc.cnt, 0)          AS count
  FROM   calendar c
  LEFT JOIN daily_counts dc ON dc.day = c.day
  ORDER BY c.day
$$;


ALTER FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT COALESCE(jsonb_agg(to_jsonb(b.*) ORDER BY b.awarded_at DESC), '[]'::jsonb)
  FROM lensers.badges b
  WHERE b.lenser_id = p_lenser_id;
$$;


ALTER FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") IS 'Security wrapper: return all badge rows for a lenser as a jsonb array ordered by awarded_at DESC.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT to_jsonb(p.*)
  FROM lensers.profiles p
  WHERE p.id = p_lenser_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") IS 'Security wrapper: full lensers.profiles row as jsonb by lenser ID.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_language_preference"() RETURNS "text"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT pr.language
  FROM lensers.preferences pr
  WHERE pr.lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_language_preference"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_language_preference"() IS 'Security wrapper: return the preferred language from the current user''s profile preferences jsonb.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text" DEFAULT NULL::"text", "p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT p.id, p.handle, p.display_name, p.avatar_url
  FROM lensers.profiles p
  WHERE
    (p_handle    IS NOT NULL AND p.handle = p_handle)
    OR
    (p_lenser_id IS NOT NULL AND p.id = p_lenser_id)
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") IS 'Security wrapper: minimal profile (id, handle, display_name, avatar_url) by handle OR lenser_id. Pass exactly one parameter.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") RETURNS "lensers"."v_lenser_profile_full"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  SELECT *
  FROM lensers.v_lenser_profile_full
  WHERE handle = lower(p_handle)
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") IS 'Returns full lenser profile data for LenserProfilePage by handle. Case-insensitive lookup via lower(). Covers stats, XP, reputation, and battle aggregates in one call.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT p.id, p.handle, p.display_name, p.avatar_url
  FROM lensers.profiles p
  WHERE p.id = ANY(p_lenser_ids);
$$;


ALTER FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) IS 'Security wrapper: batch-fetch minimal profile tuples for an array of lenser IDs.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("entity_id" "uuid", "entity_type" "text", "lenser_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT r.entity_id, r.entity_type, r.lenser_id, r.reaction, r.created_at
  FROM content.reactions r
  WHERE r.lenser_id = p_lenser_id
  ORDER BY r.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Security wrapper: paginated reaction history for a lenser, ordered by created_at DESC.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "score_type" "text", "score" numeric, "uncertainty" numeric, "computed_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  SELECT ls.id, ls.lenser_id, ls.score_type, ls.score, ls.uncertainty, ls.computed_at
  FROM reputation.lenser_scores ls
  WHERE ls.lenser_id = p_lenser_id
  ORDER BY ls.computed_at DESC;
$$;


ALTER FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") IS 'Security wrapper: return all reputation score rows for a lenser, ordered by computed_at DESC.';



CREATE OR REPLACE FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT COALESCE(
    jsonb_agg(to_jsonb(t.*) ORDER BY t.created_at DESC),
    '[]'::jsonb
  )
  FROM content.threads t
  WHERE t.lenser_id = p_lenser_id
    AND t.lenser_id = lensers.get_auth_lenser_id()
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Security wrapper: paginated private thread list owned by the current user. Returns empty array if p_lenser_id != caller.';



CREATE OR REPLACE FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "owner_lenser_id" "uuid", "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "content_text" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'media', 'lensers'
    AS $$
  SELECT
    o.id, o.workspace_id, o.owner_lenser_id, o.bucket, o.object_key,
    o.external_url, o.mime_type, o.media_type, o.name, o.content_text,
    o.byte_size, o.visibility, o.lifecycle_state, o.metadata,
    o.created_at, o.updated_at
  FROM media.objects o
  WHERE o.id = p_object_id
    AND (
      o.owner_lenser_id = lensers.get_auth_lenser_id()
      OR o.visibility   = 'public'
    );
$$;


ALTER FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") IS 'Security wrapper: fetch a media object row by ID. Returns the row if the caller owns it OR it is public. Callers must check lifecycle_state and visibility themselves.';



CREATE OR REPLACE FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer DEFAULT 400) RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'media', 'public'
    AS $$
DECLARE
  v_obj    RECORD;
  v_base   TEXT;
  v_url    TEXT;
BEGIN
  SELECT media_type, bucket, object_key, external_url, lifecycle_state
    INTO v_obj
    FROM media.objects
   WHERE id = p_media_id;

  IF NOT FOUND OR v_obj.lifecycle_state = 'deleted' THEN
    RETURN NULL;
  END IF;

  -- Use external_url as-is for non-storage objects
  IF v_obj.external_url IS NOT NULL THEN
    RETURN v_obj.external_url;
  END IF;

  -- Needs a storage object_key to generate CDN URL
  IF v_obj.bucket IS NULL OR v_obj.object_key IS NULL THEN
    RETURN NULL;
  END IF;

  v_base := current_setting('app.supabase_url', TRUE);
  IF v_base IS NULL OR v_base = '' THEN
    -- Fall back: return null so callers use original URL path
    RETURN NULL;
  END IF;

  -- Image: return webp transform URL
  IF v_obj.media_type = 'image' THEN
    v_url := v_base
      || '/storage/v1/object/public/'
      || v_obj.bucket
      || '/'
      || v_obj.object_key
      || '?width=' || p_width
      || '&format=webp&quality=80';
    RETURN v_url;
  END IF;

  -- Video and audio: return original CDN URL without transforms
  v_url := v_base
    || '/storage/v1/object/public/'
    || v_obj.bucket
    || '/'
    || v_obj.object_key;

  RETURN v_url;
END;
$$;


ALTER FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) IS 'CC: Returns CDN URL with ?width=<n>&format=webp&quality=80 for images; original URL for video/audio; NULL if not found or deleted.';



CREATE OR REPLACE FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "profile_id" "uuid", "ai_lenser_id" "uuid", "team_run_id" "uuid", "scope" "text", "source" "text", "content" "text", "confidence" numeric, "is_redacted" boolean, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
BEGIN
  IF p_workflow_id IS NULL THEN
    RETURN;
  END IF;

  RETURN QUERY
    SELECT
      m.id,
      m.profile_id,
      m.ai_lenser_id,
      m.team_run_id,
      m.scope,
      m.source,
      m.content,
      m.confidence,
      m.is_redacted,
      m.created_at
    FROM agents.memories m
    JOIN agents.team_runs tr ON tr.id = m.team_run_id
    WHERE tr.workflow_id = p_workflow_id
      AND m.is_redacted = false
    ORDER BY m.created_at DESC
    LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) IS 'Phase N3: Returns memory entries written by any team_run of the given workflow_id. Honors RLS via SECURITY INVOKER. Capped at 500 rows.';



CREATE OR REPLACE FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 50) RETURNS SETOF "battles"."model_test_runs"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
  SELECT *
    FROM battles.model_test_runs
   WHERE p_battle_id IS NULL OR battle_id = p_battle_id
   ORDER BY created_at DESC
   LIMIT LEAST(GREATEST(p_limit, 1), 200)
$$;


ALTER FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 100) RETURNS TABLE("decision_id" "uuid", "occurred_at" timestamp with time zone, "decision_type" "text", "reason" "text", "is_ai_moderated" boolean, "ai_confidence" numeric, "moderator_lenser_id" "uuid", "target_entity_schema" "text", "target_entity_table" "text", "target_entity_id" "uuid", "battle_id" "uuid", "battle_title" "text", "battle_slug" "text", "is_appealed" boolean, "appeal_outcome" "text")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'audit', 'battles', 'lensers'
    AS $$
DECLARE
  v_caller   uuid := lensers.get_auth_lenser_id();
  v_limit    int  := LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT
    md.id                           AS decision_id,
    md.occurred_at,
    md.decision_type,
    md.reason,
    md.is_ai_moderated,
    md.ai_confidence,
    md.moderator_lenser_id,
    md.target_entity_schema,
    md.target_entity_table,
    md.target_entity_id,
    b.id                            AS battle_id,
    b.title                         AS battle_title,
    b.slug                          AS battle_slug,
    md.is_appealed,
    md.appeal_outcome
  FROM audit.moderation_decisions md
  -- Resolve the owning battle whether the target is the battle row itself or
  -- a child entity (contender/submission/etc.) inside the same battle.
  LEFT JOIN battles.battles b ON (
    (md.target_entity_schema = 'battles'
       AND md.target_entity_table = 'battles'
       AND b.id = md.target_entity_id)
    OR
    (md.target_entity_schema = 'battles'
       AND md.target_entity_table IN ('contenders', 'submissions')
       AND b.id = (
         SELECT c.battle_id
         FROM battles.contenders c
         WHERE c.id = md.target_entity_id
       ))
  )
  WHERE b.creator_lenser_id = v_caller
    AND md.decision_type    = COALESCE(p_status, md.decision_type)
  ORDER BY md.occurred_at DESC
  LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) IS 'Phase P4: returns audit.moderation_decisions rows scoped to battles the caller created. SECURITY DEFINER; the audit table is otherwise private to service_role. Optional p_status filters by decision_type.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_api_keys"() RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "provider_id" "uuid", "provider_key" "text", "provider_name" "text", "label" "text", "key_suffix" "text", "is_active" boolean, "created_at" timestamp with time zone, "revoked_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM ai.fn_get_my_api_keys();
END;
$$;


ALTER FUNCTION "public"."fn_get_my_api_keys"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_api_keys"() IS 'Public REST wrapper for ai.fn_get_my_api_keys. The ai schema is not exposed in PostgREST — this wrapper is the only REST-callable entry point for listing BYOK keys.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer DEFAULT 50, "p_status" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "model_id" "uuid", "provider_key" "text", "reserved_credits" numeric, "committed_credits" numeric, "status" "text", "created_at" timestamp with time zone, "held_until" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'billing', 'agents', 'lensers', 'public', 'pg_catalog'
    AS $$
  SELECT r.id, r.ai_lenser_id, r.model_id, r.provider_key,
         r.reserved_credits, r.committed_credits, r.status,
         r.created_at, r.held_until
  FROM   billing.cost_reservations r
  JOIN   agents.ai_lensers al ON al.id = r.ai_lenser_id
  JOIN   lensers.profiles  p  ON p.id  = al.profile_id
  WHERE  p.user_id = auth.uid()
    AND  (p_status IS NULL OR r.status = p_status)
  ORDER  BY r.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") IS 'CG1: owner read of own cost reservations. Limit clamped to [1, 200].';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'ai', 'vault', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id    uuid;
  v_encrypted_id uuid;
  v_decrypted    text;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Unauthenticated: no lenser profile found';
  END IF;

  SELECT encrypted_key_id INTO v_encrypted_id
  FROM ai.keys
  WHERE id = p_key_id
    AND lenser_id = v_lenser_id
    AND is_active = true;

  IF v_encrypted_id IS NULL THEN
    RAISE EXCEPTION 'Key not found, revoked, or not owned by caller';
  END IF;

  SELECT decrypted_secret INTO v_decrypted
  FROM vault.decrypted_secrets
  WHERE id = v_encrypted_id;

  IF v_decrypted IS NULL THEN
    RAISE EXCEPTION 'Failed to decrypt key from vault';
  END IF;

  RETURN v_decrypted;
END;
$$;


ALTER FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") IS 'Decrypts a caller-owned BYOK API key from Vault. SECURITY DEFINER — runs as postgres to read vault.decrypted_secrets. Ownership enforced: lenser_id must match the authenticated caller. Only active (non-revoked) keys are returned. Works in both local and cloud Supabase environments. GUC gate removed in migration 20280116000003 — ownership check is the sufficient guard.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_lensers"() RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "type" "text", "is_active" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'agents', 'auth'
    AS $$
  -- Human profile owned directly by this auth user
  SELECT
    p.id,
    p.handle,
    p.display_name,
    p.avatar_url,
    p.type::text,
    (pref.active_lenser_id IS NULL OR pref.active_lenser_id = p.id) AS is_active
  FROM lensers.profiles p
  LEFT JOIN lensers.preferences pref ON pref.lenser_id = p.id
  WHERE p.user_id = auth.uid()
    AND p.status  = 'active'
    AND p.type    = 'human'

  UNION ALL

  -- AI lensers whose primary owner is the auth user's human profile
  SELECT
    ai_p.id,
    ai_p.handle,
    ai_p.display_name,
    ai_p.avatar_url,
    ai_p.type::text,
    (pref.active_lenser_id = ai_p.id) AS is_active
  FROM agents.ownerships   o
  JOIN agents.ai_lensers   al    ON al.id       = o.ai_lenser_id
  JOIN lensers.profiles    ai_p  ON ai_p.id     = al.profile_id
  JOIN lensers.profiles    own_p ON own_p.id    = o.owner_lenser_id
  LEFT JOIN lensers.preferences pref ON pref.lenser_id = own_p.id
  WHERE own_p.user_id   = auth.uid()
    AND o.role          = 'owner'
    AND o.revoked_at    IS NULL
    AND ai_p.status     = 'active'

  ORDER BY is_active DESC, display_name;
$$;


ALTER FUNCTION "public"."fn_get_my_lensers"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_lensers"() IS 'Returns the authenticated user''s human lenser profile plus any AI lensers they own as primary owner, each with an is_active flag indicating the current workspace selection.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_lenses"("p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "content"."visibility_enum", "title" "text", "description" "text", "author_profile" "jsonb", "reaction_totals" "jsonb", "copy_count" integer, "like_count" integer, "saved_count" integer, "tags" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'content', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RETURN; -- unauthenticated: empty result set
  END IF;

  RETURN QUERY
  SELECT
    l.id,
    l.lenser_id,
    l.visibility,
    COALESCE(et.title, 'Untitled') AS title,
    et.description,
    jsonb_build_object(
      'id',           prof.id,
      'handle',       prof.handle,
      'display_name', prof.display_name,
      'avatar_url',   prof.avatar_url
    ) AS author_profile,
    COALESCE(rt.reaction_totals, '{}') AS reaction_totals,
    COALESCE(rt.copy_count, 0)         AS copy_count,
    COALESCE(rt.like_count, 0)         AS like_count,
    COALESCE(rt.saved_count, 0)        AS saved_count,
    COALESCE(tg_agg.tags, '[]')        AS tags,
    l.created_at
  FROM lenses.lenses l
  LEFT JOIN content.entity_translations et
    ON et.entity_id = l.id
   AND et.entity_type = 'lens'
   AND et.is_original = true
  LEFT JOIN lensers.profiles prof ON prof.id = l.lenser_id
  LEFT JOIN LATERAL (
    SELECT
      COALESCE(jsonb_object_agg(x.reaction, x.cnt), '{}')::jsonb  AS reaction_totals,
      COALESCE(SUM(CASE WHEN x.reaction = 'copy'::content.reaction_enum  THEN x.cnt ELSE 0 END)::integer, 0) AS copy_count,
      COALESCE(SUM(CASE WHEN x.reaction = 'like'::content.reaction_enum  THEN x.cnt ELSE 0 END)::integer, 0) AS like_count,
      COALESCE(SUM(CASE WHEN x.reaction = 'saved'::content.reaction_enum THEN x.cnt ELSE 0 END)::integer, 0) AS saved_count
    FROM (
      SELECT rx.reaction, COUNT(*)::integer AS cnt
      FROM content.reactions rx
      WHERE rx.entity_type = 'lens' AND rx.entity_id = l.id
      GROUP BY rx.reaction
    ) x
  ) rt ON true
  LEFT JOIN LATERAL (
    SELECT COALESCE(jsonb_agg(
      jsonb_build_object('id', tg.id, 'slug', tg.slug, 'name', tg.slug)
    ), '[]') AS tags
    FROM content.tag_map tm
    JOIN content.tags tg ON tg.id = tm.tag_id
    WHERE tm.entity_type = 'lens' AND tm.entity_id = l.id
  ) tg_agg ON true
  WHERE l.lenser_id = v_lenser_id
  ORDER BY l.created_at DESC
  OFFSET GREATEST(p_offset, 0)
  LIMIT  LEAST(p_limit, 100);
END;
$$;


ALTER FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) IS 'Returns all lenses (public, private, unlisted) owned by the authenticated lenser. Used by the "My Lenses" filter on LensesPage.';



CREATE OR REPLACE FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") RETURNS TABLE("vote_value" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  SELECT v.vote_value::text
  FROM battles.votes v
  WHERE v.battle_id       = p_battle_id
    AND v.voter_lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 12, "p_visibility" "text" DEFAULT NULL::"text", "p_sort" "text" DEFAULT 'updated_at'::"text", "p_search" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "node_count" bigint, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility::text,
    w.battle_count,
    (
      SELECT count(*)
      FROM lenses.workflow_nodes wn
      WHERE wn.workflow_id = w.id
    ) AS node_count,
    w.reaction_totals,
    w.fork_count,
    w.parent_workflow_id,
    w.created_at,
    w.updated_at
  FROM lenses.workflows w
  WHERE w.lenser_id = p_lenser_id
    AND (p_visibility IS NULL OR w.visibility::text = p_visibility)
    AND (p_search IS NULL OR w.title ILIKE '%' || p_search || '%')
  ORDER BY
    CASE WHEN p_sort = 'updated_at' THEN EXTRACT(EPOCH FROM w.updated_at) END DESC,
    CASE WHEN p_sort = 'created_at' THEN EXTRACT(EPOCH FROM w.created_at) END DESC,
    CASE WHEN p_sort = 'battle_count' THEN w.battle_count::float END DESC NULLS LAST,
    w.updated_at DESC
  LIMIT p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_notification_preferences"() RETURNS TABLE("notification_type" "text", "enabled" boolean, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
BEGIN
  RETURN QUERY
    SELECT np.notification_type, np.enabled, np.updated_at
    FROM   public.notification_preferences np
    WHERE  np.lenser_id = v_lenser_id
    ORDER  BY np.notification_type;
END;
$$;


ALTER FUNCTION "public"."fn_get_notification_preferences"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_notifications"("p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "type" "text", "title" "text", "body" "text", "action_url" "text", "metadata" "jsonb", "read_at" timestamp with time zone, "created_at" timestamp with time zone, "unread_count" bigint)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_unread    BIGINT;
BEGIN
  SELECT COUNT(*) INTO v_unread
  FROM   public.notifications AS _n
  WHERE  _n.lenser_id = v_lenser_id
  AND    _n.read_at IS NULL;

  RETURN QUERY
    SELECT
      n.id, n.type, n.title, n.body, n.action_url, n.metadata, n.read_at, n.created_at,
      v_unread
    FROM   public.notifications n
    WHERE  n.lenser_id = v_lenser_id
    AND    (p_cursor IS NULL OR n.created_at < p_cursor)
    ORDER  BY n.created_at DESC
    LIMIT  p_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_pending_requests"("p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "source_profile_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "requested_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT
    r.id,
    r.source_profile_id,
    p.handle,
    p.display_name,
    p.avatar_url,
    r.requested_at
  FROM lensers.relationships r
  JOIN lensers.profiles p ON p.id = r.source_profile_id
  JOIN lensers.profiles me ON me.user_id = auth.uid() AND me.id = r.target_profile_id
  WHERE r.status = 'pending'
    AND p.status = 'active'
  ORDER BY r.requested_at DESC
  LIMIT LEAST(p_limit, 100)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") RETURNS SETOF "agents"."provider_configs"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT *
  FROM   agents.provider_configs
  WHERE  ai_lenser_id = p_ai_lenser_id
    AND  agents.can_manage_ai_lenser(p_ai_lenser_id)
  ORDER BY provider_key;
$$;


ALTER FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") IS 'Returns provider configs for a given AI lenser. Authorization via can_manage_ai_lenser — returns empty set if caller is not the owner. Also used by the test-provider Edge Function for ownership validation + base_url lookup.';



CREATE OR REPLACE FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer DEFAULT 20) RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT COALESCE(jsonb_agg(to_jsonb(p.*) ORDER BY p.last_active_at DESC), '[]'::jsonb)
  FROM lensers.profiles p
  WHERE p.status = 'active'
    AND p.deletion_requested_at IS NULL
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) IS 'Security wrapper: return recently active lensers as a jsonb array, max 100.';



CREATE OR REPLACE FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) RETURNS TABLE("id" "uuid", "title" "text", "description" "text", "weight" numeric)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT rc.id, rc.title, rc.description, rc.weight
  FROM battles.rubric_criteria rc
  WHERE rc.id = ANY(p_criterion_ids);
$$;


ALTER FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer DEFAULT 30) RETURNS TABLE("rule_id" "uuid", "dispatched_count" bigint, "failed_count" bigint, "skipped_count" bigint, "queued_count" bigint, "last_attempted_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  SELECT
    ed.rule_id,
    COUNT(*) FILTER (WHERE ed.status = 'dispatched') AS dispatched_count,
    COUNT(*) FILTER (WHERE ed.status = 'failed')     AS failed_count,
    COUNT(*) FILTER (WHERE ed.status = 'skipped')    AS skipped_count,
    COUNT(*) FILTER (WHERE ed.status = 'queued')     AS queued_count,
    MAX(ed.attempted_at)                             AS last_attempted_at
  FROM automation.event_dispatches ed
  JOIN automation.trigger_rules tr ON tr.id = ed.rule_id
  WHERE tr.lenser_id = auth.uid()
    AND ed.attempted_at >= (now() - (COALESCE(p_days, 30) || ' days')::interval)
  GROUP BY ed.rule_id;
$$;


ALTER FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) IS 'Security wrapper: aggregate dispatch counts per rule for the current user over the last p_days days (default 30). Replaces client-side fan-out query.';



CREATE OR REPLACE FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "request_id" "uuid", "status" "text", "model_id" "uuid", "model_key" "text", "provider_key" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "latency_ms" integer, "token_input" integer, "token_output" integer, "credit_cost" bigint, "billing_status" "text", "error_code" "text", "error_message" "text", "artifacts" "jsonb")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM execution.fn_get_run_details(p_run_id);
END;
$$;


ALTER FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") IS 'Security wrapper: get execution run details including aggregated artifacts. Delegates auth enforcement to execution.fn_get_run_details.';



CREATE OR REPLACE FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "direction" "text", "source_run_id" "uuid", "source_workflow_id" "uuid", "source_node_id" "uuid", "source_output_path" "text", "target_run_id" "uuid", "target_workflow_id" "uuid", "target_node_id" "uuid", "target_input_path" "text", "transform" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
  SELECT
    p.id,
    CASE WHEN p.target_run_id = p_run_id THEN 'upstream' ELSE 'downstream' END AS direction,
    p.source_run_id,
    sr.workflow_id AS source_workflow_id,
    p.source_node_id,
    p.source_output_path,
    p.target_run_id,
    tr.workflow_id AS target_workflow_id,
    p.target_node_id,
    p.target_input_path,
    p.transform,
    p.created_at
  FROM lenses.workflow_run_provenance p
  JOIN lenses.workflow_runs sr ON sr.id = p.source_run_id
  JOIN lenses.workflow_runs tr ON tr.id = p.target_run_id
  WHERE (p.source_run_id = p_run_id OR p.target_run_id = p_run_id)
    AND EXISTS (
      SELECT 1
      FROM lenses.workflow_runs r
      JOIN lenses.workflows w ON w.id = r.workflow_id
      WHERE r.id = p_run_id
        AND (
          r.triggered_by = lensers.get_auth_lenser_id()
          OR w.visibility::text = 'public'
        )
    )
  ORDER BY p.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") IS 'Returns the provenance edges where this run is either the source or the target. Drives the n8n-style "data came from / data used by" tabs in the inspector.';



CREATE OR REPLACE FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(rr.*)
  FROM agents.run_reports rr
  WHERE rr.id = p_report_id
    AND (
      rr.ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
          AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_schedule_calendars"() RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "name" "text", "kind" "text", "dates" "date"[], "timezone" "text", "is_seed" boolean, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT sc.id, sc.lenser_id, sc.name, sc.kind, sc.dates,
         sc.timezone, sc.is_seed, sc.created_at
  FROM lenses.schedule_calendars sc
  WHERE sc.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY sc.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_get_schedule_calendars"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_schedule_calendars"() IS 'Security wrapper: list all schedule calendars owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "schedule_id" "uuid", "status" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    wr.id, wr.workflow_id, wr.schedule_id, wr.status,
    wr.started_at, wr.completed_at, wr.created_at
  FROM lenses.workflow_runs wr
  JOIN lenses.lenses l ON l.id = wr.workflow_id
  WHERE wr.workflow_id = p_workflow_id
    AND l.lenser_id = lensers.get_auth_lenser_id()
    AND (p_cursor IS NULL OR wr.created_at < p_cursor)
  ORDER BY wr.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: keyset-paginated run history for a workflow owned by the current user. Max 100 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid" DEFAULT '00000000-0000-0000-0000-000000000001'::"uuid", "p_season_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("season_id" "uuid", "season_slug" "text", "app_id" "uuid", "rank" bigint, "lenser_id" "uuid", "total_xp" bigint, "user" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
  SELECT
    v.season_id,
    v.season_slug,
    v.app_id,
    v.rank,
    v.lenser_id,
    v.total_xp,
    v."user"
  FROM public.vw_xp_leaderboard_season v
  WHERE v.app_id = p_app_id
    AND (p_season_id IS NULL OR v.season_id = p_season_id)
  ORDER BY v.rank ASC
  LIMIT p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_series"("p_series_id" "uuid") RETURNS TABLE("series_id" "uuid", "title" "text", "template_id" "uuid", "creator_lenser_id" "uuid", "round_count" integer, "current_round" integer, "status" "text", "round_number" integer, "battle_id" "uuid", "battle_slug" "text", "battle_status" "text", "winner_contender_id" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
  SELECT s.id, s.title, s.template_id, s.creator_lenser_id, s.round_count,
         s.current_round, s.status,
         sr.round_number, sr.battle_id, b.slug, b.status::text, sr.winner_contender_id
    FROM battles.series s
    LEFT JOIN battles.series_rounds sr ON sr.series_id = s.id
    LEFT JOIN battles.battles b ON b.id = sr.battle_id
   WHERE s.id = p_series_id
     AND (
       s.creator_lenser_id = auth.uid()
       OR b.status::text = 'published'
     )
   ORDER BY sr.round_number;
$$;


ALTER FUNCTION "public"."fn_get_series"("p_series_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(tm.*)
  FROM agents.team_members tm
  JOIN agents.teams t ON t.id = tm.team_id
  WHERE tm.team_id = p_team_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = t.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY tm.lane, tm.sort_order;
$$;


ALTER FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT to_jsonb(t.*)
  FROM content.threads t
  WHERE t.id = p_thread_id
    AND t.lenser_id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") IS 'Security wrapper: fetch a private/draft thread owned by the current user. Returns NULL if not found or not owned.';



CREATE OR REPLACE FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "thread_id" "uuid", "parent_reply_id" "uuid", "lenser_id" "uuid", "content" "text", "content_html" "text", "reaction_totals" "jsonb", "created_at" timestamp with time zone, "author_profile" "jsonb")
    LANGUAGE "sql" STABLE
    SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers', 'auth'
    AS $$
  WITH RECURSIVE root_page AS (
    -- Select only top-level (root) replies for this page
    SELECT r.id
    FROM content.thread_replies r
    WHERE r.thread_id     = p_thread_id
      AND r.parent_reply_id IS NULL
      AND r.status        = 'published'
      AND r.deleted_at    IS NULL
    ORDER BY r.created_at ASC
    LIMIT  LEAST(p_limit, 50)
    OFFSET p_offset
  ),
  descendants AS (
    -- Seed: the root replies for this page
    SELECT r.id, r.parent_reply_id
    FROM content.thread_replies r
    WHERE r.id IN (SELECT id FROM root_page)
    UNION ALL
    -- Recurse: children of already-selected replies
    SELECT r.id, r.parent_reply_id
    FROM content.thread_replies r
    INNER JOIN descendants d ON r.parent_reply_id = d.id
    WHERE r.status     = 'published'
      AND r.deleted_at IS NULL
  )
  SELECT
    v.id,
    v.thread_id,
    v.parent_reply_id,
    v.lenser_id,
    v.content,
    v.content_html,
    v.reaction_totals,
    v.created_at,
    v.author_profile
  FROM public.vw_content_thread_replies_public v
  WHERE v.id IN (SELECT id FROM descendants)
  ORDER BY v.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Paginated thread replies. Paginates by root reply count (LIMIT/OFFSET on root-level entries), then recursively fetches all descendants of the selected roots. Hard cap: 50 root replies per call. Uses SECURITY DEFINER so anon can read public threads without a direct grant on lensers.profiles.';



CREATE OR REPLACE FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "thread_id" "uuid", "parent_reply_id" "uuid", "lenser_id" "uuid", "content" "text", "created_at" timestamp with time zone, "deleted_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT r.id, r.thread_id, r.parent_reply_id, r.lenser_id,
         r.content, r.created_at, r.deleted_at
  FROM content.thread_replies r
  JOIN content.threads t ON t.id = r.thread_id
  WHERE r.thread_id = p_thread_id
    AND t.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY r.created_at ASC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Security wrapper: paginated replies for a private thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer DEFAULT 7) RETURNS TABLE("workflow_id" "uuid", "workflow_title" "text", "tool_name" "text", "total_invocations" bigint, "approved_count" bigint, "rejected_count" bigint, "last_invoked_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE
    SET "search_path" TO 'public', 'platform', 'lenses', 'agents'
    AS $$
DECLARE
  v_window int := GREATEST(LEAST(COALESCE(p_days, 7), 90), 1);
BEGIN
  IF p_ai_lenser_id IS NULL THEN
    RAISE EXCEPTION 'ai_lenser_id required' USING ERRCODE = '22023';
  END IF;

  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not_owner_of_ai_lenser' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT
    wr.workflow_id,
    w.title                                            AS workflow_title,
    til.tool_name,
    count(*)::bigint                                   AS total_invocations,
    count(*) FILTER (
      WHERE til.approval_status IN ('approved', 'auto_approved')
    )::bigint                                          AS approved_count,
    count(*) FILTER (
      WHERE til.approval_status = 'rejected'
    )::bigint                                          AS rejected_count,
    max(til.invoked_at)                                AS last_invoked_at
  FROM   platform.tool_invocation_logs til
  LEFT JOIN lenses.workflow_runs wr ON wr.id = til.run_id
  LEFT JOIN lenses.workflows     w  ON w.id  = wr.workflow_id
  WHERE  til.ai_lenser_id = p_ai_lenser_id
    AND  til.invoked_at >= now() - (v_window::text || ' days')::interval
  GROUP  BY wr.workflow_id, w.title, til.tool_name
  ORDER  BY total_invocations DESC, last_invoked_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) IS 'Phase Q4: per-(workflow,tool_name) rollup of platform.tool_invocation_logs over the last p_days (default 7, hard-capped to [1,90]). SECURITY INVOKER preserves underlying RLS; explicit can_manage_ai_lenser guard prevents cross-owner enumeration via the workflow JOIN.';



CREATE OR REPLACE FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") RETURNS TABLE("round_number" integer, "round_status" "text", "match_id" "uuid", "battle_id" "uuid", "battle_slug" "text", "contender_a_lenser_id" "uuid", "contender_a_handle" "text", "contender_a_avatar_url" "text", "contender_b_lenser_id" "uuid", "contender_b_handle" "text", "contender_b_avatar_url" "text", "winner_lenser_id" "uuid", "winner_handle" "text", "winner_avatar_url" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
  SELECT
    tr.round_number,
    tr.status::TEXT          AS round_status,
    tm.id                    AS match_id,
    tm.battle_id,
    b.slug                   AS battle_slug,
    tca.lenser_id            AS contender_a_lenser_id,
    pa.handle                AS contender_a_handle,
    pa.avatar_url            AS contender_a_avatar_url,
    tcb.lenser_id            AS contender_b_lenser_id,
    pb.handle                AS contender_b_handle,
    pb.avatar_url            AS contender_b_avatar_url,
    tcw.lenser_id            AS winner_lenser_id,
    pw.handle                AS winner_handle,
    pw.avatar_url            AS winner_avatar_url
  FROM battles.tournament_rounds tr
  JOIN  battles.tournament_matches tm    ON tm.round_id = tr.id
  LEFT JOIN battles.battles b            ON b.id = tm.battle_id
  LEFT JOIN battles.tournament_contenders tca ON tca.id = tm.contender_a_id
  LEFT JOIN battles.tournament_contenders tcb ON tcb.id = tm.contender_b_id
  LEFT JOIN battles.tournament_contenders tcw ON tcw.id = tm.winner_tournament_contender_id
  LEFT JOIN lensers.profiles pa ON pa.id = tca.lenser_id
  LEFT JOIN lensers.profiles pb ON pb.id = tcb.lenser_id
  LEFT JOIN lensers.profiles pw ON pw.id = tcw.lenser_id
  WHERE tr.tournament_id = p_tournament_id
  ORDER BY tr.round_number, tm.created_at;
$$;


ALTER FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT to_jsonb(t.*)
  FROM battles.tournaments t
  WHERE t.slug = p_slug
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT to_jsonb(tc.*)
  FROM battles.tournament_contenders tc
  WHERE tc.tournament_id = p_tournament_id
  ORDER BY tc.seed NULLS LAST, tc.created_at;
$$;


ALTER FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_trending_battles"("p_limit" integer DEFAULT 20, "p_cursor" numeric DEFAULT NULL::numeric) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "published_at" timestamp with time zone, "battle_type" "text", "total_vote_count" integer, "vote_velocity" numeric, "og_image_url" "text", "contender_a_name" "text", "contender_b_name" "text", "winner_slot" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
  SELECT
    b.id, b.slug, b.title, b.status::TEXT, b.published_at,
    b.battle_type::TEXT, b.total_vote_count, b.vote_velocity, b.og_image_url,
    ca.display_name AS contender_a_name,
    cb.display_name AS contender_b_name,
    CASE
      WHEN b.winner_contender_id = ca.id THEN 'A'
      WHEN b.winner_contender_id = cb.id THEN 'B'
      ELSE NULL
    END AS winner_slot
  FROM battles.battles b
  LEFT JOIN battles.contenders ca ON ca.battle_id = b.id AND ca.slot = 'A'
  LEFT JOIN battles.contenders cb ON cb.battle_id = b.id AND cb.slot = 'B'
  WHERE b.status = 'published'
  AND   b.published_at >= now() - INTERVAL '7 days'
  AND   (p_cursor IS NULL OR b.vote_velocity < p_cursor)
  ORDER BY b.vote_velocity DESC, b.total_vote_count DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_unread_notification_count"() RETURNS bigint
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT COUNT(*)::BIGINT
  FROM   public.notifications
  WHERE  lenser_id = lensers.get_auth_lenser_id()
  AND    read_at IS NULL;
$$;


ALTER FUNCTION "public"."fn_get_unread_notification_count"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") RETURNS TABLE("entity_id" "uuid", "reaction" "text", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT r.entity_id, r.reaction, r.created_at
  FROM content.reactions r
  WHERE r.entity_type::text = p_entity_type
    AND r.entity_id   = p_entity_id
    AND r.lenser_id   = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") IS 'Security wrapper: return the current user''s reaction row for a single entity.';



CREATE OR REPLACE FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") RETURNS TABLE("version_id" "uuid", "input_contract" "jsonb", "output_contract" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_temp'
    AS $$
  select v.id, v.input_contract, v.output_contract
  from lenses.versions v
  where v.id = p_version_id
$$;


ALTER FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") IS 'Returns the input_contract + output_contract JSONB for a lens version in a single round-trip. Consumed by the workflow execution engine.';



CREATE OR REPLACE FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") RETURNS TABLE("battle_id" "uuid", "contender_id" "uuid", "raw_vote_count" integer, "weighted_vote_sum" numeric, "draw_count" integer, "rank_position" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT va.battle_id, va.contender_id, va.raw_vote_count,
         va.weighted_vote_sum, va.draw_count, va.rank_position
  FROM battles.vote_aggregates va
  WHERE va.battle_id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") RETURNS TABLE("workflow" "jsonb", "nodes" "jsonb", "edges" "jsonb")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers', 'content', 'pg_temp'
    AS $$
DECLARE
  v_viewer_id uuid;
BEGIN
  v_viewer_id := lensers.get_auth_lenser_id();

  RETURN QUERY
  WITH wf AS (
    SELECT * FROM public.fn_get_workflow_detail(p_workflow_id)
  ),
  viewer_reacts AS (
    SELECT
      COALESCE(jsonb_object_agg(r.reaction::text, true), '{}'::jsonb) AS reactions
    FROM content.reactions r
    WHERE v_viewer_id IS NOT NULL
      AND r.entity_type::text = 'workflow'
      AND r.entity_id          = p_workflow_id
      AND r.lenser_id          = v_viewer_id
  )
  SELECT
    to_jsonb(wf.*) || jsonb_build_object(
      'viewer_reactions', COALESCE((SELECT reactions FROM viewer_reacts), '{}'::jsonb)
    ) AS workflow,
    COALESCE(
      (SELECT jsonb_agg(n) FROM public.fn_get_workflow_nodes(p_workflow_id) n),
      '[]'::jsonb
    ) AS nodes,
    COALESCE(
      (SELECT jsonb_agg(e) FROM public.fn_get_workflow_edges(p_workflow_id) e),
      '[]'::jsonb
    ) AS edges
  FROM wf;
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") IS 'Bootstrap for WorkflowBuilderPage. Returns (workflow, nodes, edges) in one call. The workflow jsonb additionally carries viewer_reactions={reaction:true,...} so the page avoids a separate fn_get_entity_reaction_counts + fn_get_entity_reaction_status round trip.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "author_profile" "jsonb", "parent_workflow_title" "text", "parent_workflow_author_profile" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
WITH caller AS (
  SELECT lensers.get_auth_lenser_id() AS id
)
SELECT
  w.id,
  w.lenser_id,
  w.title,
  w.description,
  w.visibility::text,
  w.battle_count,
  w.reaction_totals,
  w.fork_count,
  w.parent_workflow_id,
  CASE
    WHEN prof.id IS NULL THEN NULL
    ELSE jsonb_build_object(
      'id', prof.id,
      'handle', prof.handle,
      'display_name', prof.display_name,
      'avatar_url', prof.avatar_url
    )
  END AS author_profile,
  parent.title AS parent_workflow_title,
  CASE
    WHEN parent_prof.id IS NULL THEN NULL
    ELSE jsonb_build_object(
      'id', parent_prof.id,
      'handle', parent_prof.handle,
      'display_name', parent_prof.display_name,
      'avatar_url', parent_prof.avatar_url
    )
  END AS parent_workflow_author_profile,
  w.created_at,
  w.updated_at
FROM lenses.workflows w
LEFT JOIN lensers.profiles prof
  ON prof.id = w.lenser_id
LEFT JOIN lenses.workflows parent
  ON parent.id = w.parent_workflow_id
LEFT JOIN lensers.profiles parent_prof
  ON parent_prof.id = parent.lenser_id
WHERE w.id = p_workflow_id
  AND (
    w.visibility = 'public'
    OR w.lenser_id = (SELECT id FROM caller)
  );
$$;


ALTER FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") IS 'Returns a workflow with denormalized author and parent workflow metadata for the builder page.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "source_node_id" "uuid", "target_node_id" "uuid", "source_output_key" "text", "target_param_label" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    e.id,
    e.workflow_id,
    e.source_node_id,
    e.target_node_id,
    e.source_output_key,
    e.target_param_label
  FROM lenses.workflow_edges e
  JOIN lenses.workflows w ON w.id = e.workflow_id
  WHERE e.workflow_id = p_workflow_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
    );
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") IS 'Returns all edges for a workflow. Public workflows are readable by anyone; private/unlisted workflows are readable only by the owner.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "run_id" "uuid", "node_id" "uuid", "execution_run_id" "uuid", "status" "text", "output_data" "jsonb", "error_message" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    nr.id,
    nr.run_id,
    nr.node_id,
    nr.execution_run_id,
    nr.status,
    nr.output_data,
    nr.error_message,
    nr.started_at,
    nr.completed_at
  FROM lenses.workflow_node_results nr
  JOIN lenses.workflow_runs r ON r.id = nr.run_id
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE nr.run_id = p_run_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
      OR r.triggered_by = lensers.get_auth_lenser_id()
    );
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") IS 'Returns all node results for a workflow run. Accessible to run owner or anyone for public workflows.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "position_x" double precision, "position_y" double precision, "label" "text", "ordinal" integer, "created_at" timestamp with time zone, "config" "jsonb", "lens_visibility" "text", "lens_lenser_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    n.id,
    n.workflow_id,
    n.lens_id,
    n.version_id,
    n.position_x,
    n.position_y,
    n.label,
    n.ordinal,
    n.created_at,
    n.config,
    l.visibility::text  AS lens_visibility,
    l.lenser_id         AS lens_lenser_id
  FROM lenses.workflow_nodes n
  JOIN lenses.workflows w ON w.id = n.workflow_id
  LEFT JOIN lenses.lenses l ON l.id = n.lens_id
  WHERE n.workflow_id = p_workflow_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
    )
  ORDER BY n.ordinal, n.created_at;
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") IS 'Returns workflow nodes enriched with lens visibility and owner. Public workflows readable by anyone; private/unlisted only by owner.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "triggered_by" "uuid", "status" "text", "context_inputs" "jsonb", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    r.id,
    r.workflow_id,
    r.triggered_by,
    r.status,
    r.context_inputs,
    r.started_at,
    r.completed_at,
    r.created_at
  FROM lenses.workflow_runs r
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE r.id = p_run_id
    AND (
      w.visibility = 'public'
      OR w.lenser_id = lensers.get_auth_lenser_id()
      OR r.triggered_by = lensers.get_auth_lenser_id()
    );
END;
$$;


ALTER FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") IS 'Fetches a single workflow run record. Accessible to the run owner or anyone for public workflows.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") RETURNS TABLE("media_manifest" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT wr.media_manifest
  FROM lenses.workflow_runs wr
  WHERE wr.id = p_run_id
    AND wr.triggered_by = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") IS 'Security wrapper: get the media_manifest jsonb array for a workflow run owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") RETURNS TABLE("run_id" "uuid", "workflow_id" "uuid", "status" "text", "active_node_id" "uuid", "pending_count" integer, "waiting_count" integer, "in_flight_count" integer, "executed_count" integer, "failed_count" integer, "is_running" boolean, "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "parent_run_id" "uuid", "recursion_depth" integer, "node_results" "jsonb", "upstream_count" integer, "downstream_count" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
  WITH run AS (
    SELECT *
    FROM lenses.workflow_runs r
    WHERE r.id = p_run_id
      AND EXISTS (
        SELECT 1
        FROM lenses.workflows w
        WHERE w.id = r.workflow_id
          AND (
            r.triggered_by = lensers.get_auth_lenser_id()
            OR w.visibility::text = 'public'
          )
      )
  ),
  results AS (
    SELECT
      nr.*,
      n.label    AS node_label,
      n.ordinal  AS node_ordinal
    FROM lenses.workflow_node_results nr
    LEFT JOIN lenses.workflow_nodes n ON n.id = nr.node_id
    WHERE nr.run_id = p_run_id
  ),
  agg AS (
    SELECT
      COUNT(*) FILTER (WHERE results.status = 'pending')                                                     AS pending_count,
      COUNT(*) FILTER (WHERE results.status IN ('awaiting_dependency', 'queued'))                            AS waiting_count,
      COUNT(*) FILTER (WHERE results.status IN ('running', 'streaming', 'retrying'))                         AS in_flight_count,
      COUNT(*) FILTER (WHERE results.status IN ('completed', 'skipped'))                                     AS executed_count,
      COUNT(*) FILTER (WHERE results.status IN ('failed', 'timed_out', 'blocked', 'invalidated', 'cancelled')) AS failed_count
    FROM results
  ),
  prov AS (
    SELECT
      COUNT(*) FILTER (WHERE p.target_run_id = p_run_id) AS upstream_count,
      COUNT(*) FILTER (WHERE p.source_run_id = p_run_id) AS downstream_count
    FROM lenses.workflow_run_provenance p
    WHERE p.target_run_id = p_run_id OR p.source_run_id = p_run_id
  ),
  ordered_results AS (
    SELECT jsonb_agg(
      jsonb_build_object(
        'id',              results.id,
        'node_id',         results.node_id,
        'node_label',      results.node_label,
        'node_ordinal',    results.node_ordinal,
        'status',          results.status,
        'waiting_reason',  results.waiting_reason,
        'output_data',     results.output_data,
        'error_message',   results.error_message,
        'retry_count',     results.retry_count,
        'duration_ms',     results.duration_ms,
        'ttfb_ms',         results.ttfb_ms,
        'started_at',      results.started_at,
        'completed_at',    results.completed_at
      )
      ORDER BY results.node_ordinal NULLS LAST, results.id
    ) AS data
    FROM results
  )
  SELECT
    run.id,
    run.workflow_id,
    run.status::text,
    run.active_node_id,
    COALESCE(agg.pending_count, 0)::int,
    COALESCE(agg.waiting_count, 0)::int,
    COALESCE(agg.in_flight_count, 0)::int,
    COALESCE(agg.executed_count, 0)::int,
    COALESCE(agg.failed_count, 0)::int,
    (run.status NOT IN ('completed', 'failed', 'cancelled', 'timed_out')) AS is_running,
    run.started_at,
    run.completed_at,
    run.parent_run_id,
    run.recursion_depth,
    COALESCE(ordered_results.data, '[]'::jsonb),
    COALESCE(prov.upstream_count, 0)::int,
    COALESCE(prov.downstream_count, 0)::int
  FROM run
  CROSS JOIN agg
  CROSS JOIN prov
  CROSS JOIN ordered_results;
$$;


ALTER FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") IS 'N8N-style canonical run projection. One round-trip returns active node, waiting/executed counts, ordered node results, and provenance edge counts. Drives the workflow execution inspector.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "status" "text", "scheduled_for" timestamp with time zone, "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    wr.id,
    wr.workflow_id,
    wr.status,
    wr.scheduled_for,
    wr.started_at,
    wr.completed_at,
    wr.created_at
  FROM lenses.workflow_runs wr
  JOIN lenses.workflow_schedules ws ON ws.id = wr.schedule_id
  JOIN lenses.workflows w ON w.id = wr.workflow_id
  WHERE wr.schedule_id = p_schedule_id
    AND wr.scheduled_for IS NOT NULL
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wr.scheduled_for DESC
  LIMIT 50;
$$;


ALTER FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") IS 'Returns the 50 most recent scheduled workflow_runs for a given schedule, newest first.
   Accessible only to the workflow owner via lensers.get_auth_lenser_id().';



CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "workflow_title" "text", "cron_expr" "text", "timezone" "text", "global_model_id" "text", "inputs_template" "jsonb", "is_active" boolean, "assignee_type" "text", "assignee_id" "uuid", "workflow_assignment_id" "uuid", "approval_policy" "jsonb", "retry_policy" "jsonb", "failure_policy" "jsonb", "queue_policy" "jsonb", "next_run_at" timestamp with time zone, "last_run_at" timestamp with time zone, "last_run_id" "uuid", "last_dispatch_status" "text", "last_error_at" timestamp with time zone, "last_error_message" "text", "last_completed_at" timestamp with time zone, "last_result" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    s.id,
    s.workflow_id,
    w.title AS workflow_title,
    s.cron_expr,
    s.timezone,
    s.global_model_id,
    s.inputs_template,
    s.is_active,
    s.assignee_type,
    s.assignee_id,
    s.workflow_assignment_id,
    s.approval_policy,
    s.retry_policy,
    s.failure_policy,
    s.queue_policy,
    s.next_run_at,
    s.last_run_at,
    s.last_run_id,
    s.last_dispatch_status,
    s.last_error_at,
    s.last_error_message,
    s.last_completed_at,
    s.last_result,
    s.created_at
  FROM lenses.workflow_schedules s
  JOIN lenses.workflows w ON w.id = s.workflow_id
  WHERE w.lenser_id = lensers.get_auth_lenser_id()
    AND (p_workflow_id IS NULL OR s.workflow_id = p_workflow_id)
  ORDER BY s.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "version_number" integer, "changelog" "text", "status" "text", "published_at" timestamp with time zone, "created_by" "uuid", "created_at" timestamp with time zone, "node_count" bigint, "edge_count" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
  SELECT * FROM lenses.fn_get_workflow_versions(p_workflow_id);
$$;


ALTER FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") IS 'Public wrapper: returns versioned snapshots for a workflow the caller can access. Delegates to lenses.fn_get_workflow_versions which enforces ownership/visibility via RLS.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50) RETURNS TABLE("run_id" "uuid", "run_type" "text", "ai_lenser_id" "uuid", "status" "text", "approval_status" "text", "total_cost" numeric, "step_count" bigint, "memory_write_count" bigint, "latest_evaluation_score" numeric, "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "duration_seconds" numeric)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lenses', 'lensers', 'auth'
    AS $$
DECLARE
  v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
BEGIN
  IF p_ai_lenser_id IS NULL THEN
    RETURN;
  END IF;

  RETURN QUERY
  SELECT
    v.run_id,
    v.run_type,
    v.ai_lenser_id,
    v.status,
    v.approval_status,
    v.total_cost,
    v.step_count,
    v.memory_write_count,
    v.latest_evaluation_score,
    v.started_at,
    v.completed_at,
    v.duration_seconds
  FROM agents.v_run_unified v
  WHERE v.ai_lenser_id = p_ai_lenser_id
  ORDER BY v.started_at DESC NULLS LAST
  LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) IS 'Public SECURITY DEFINER wrapper around agents.v_run_unified. Returns up to p_limit (max 200, default 50) unified runs for the given AI agent. RLS enforcement is via agents.can_manage_ai_lenser inside the view. Returns empty set instead of 404/500 for unknown or inaccessible agents.';



CREATE OR REPLACE FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ws.*)
  FROM agents.workspace_settings ws
  WHERE ws.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_gate_kind" "text" DEFAULT NULL::"text", "p_hours" integer DEFAULT 24) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_caller     uuid := lensers.get_auth_lenser_id();
  v_id         uuid;
  v_expires    timestamptz;
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not_owner_of_ai_lenser' USING ERRCODE = '42501';
  END IF;

  IF p_workflow_id IS NULL AND (p_gate_kind IS NULL OR length(trim(p_gate_kind)) = 0) THEN
    RAISE EXCEPTION 'standing approval requires workflow_id or gate_kind' USING ERRCODE = '22023';
  END IF;

  v_expires := CASE
    WHEN p_hours IS NULL OR p_hours <= 0 THEN NULL
    ELSE now() + (p_hours::text || ' hours')::interval
  END;

  INSERT INTO agents.standing_approvals (
    ai_lenser_id, workflow_id, gate_kind, expires_at, created_by
  ) VALUES (
    p_ai_lenser_id, p_workflow_id, NULLIF(trim(p_gate_kind), ''), v_expires, v_caller
  )
  RETURNING id INTO v_id;

  INSERT INTO agents.action_logs (
    ai_lenser_id, action_type, result, metadata
  ) VALUES (
    p_ai_lenser_id,
    'standing_approval_granted',
    'success',
    jsonb_build_object(
      'standing_approval_id', v_id,
      'workflow_id',          p_workflow_id,
      'gate_kind',            NULLIF(trim(p_gate_kind), ''),
      'expires_at',            v_expires,
      'granted_by',            v_caller
    )
  );

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) IS 'Phase Q2: grants a standing approval for an AI lenser. p_hours<=0 or NULL means no expiry. Returns the standing_approvals.id. Owner-only via agents.can_manage_ai_lenser; writes an action_logs entry.';



CREATE OR REPLACE FUNCTION "public"."fn_health"() RETURNS integer
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT 1;
$$;


ALTER FUNCTION "public"."fn_health"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_health"() IS 'Phase K4 health probe. Returns 1. Called by the platform-api GET /health route to verify database reachability.';



CREATE OR REPLACE FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  UPDATE lenses.workflow_runs
  SET heartbeat_at  = now(),
      run_worker_id = COALESCE(run_worker_id, p_worker_id)
  WHERE id = p_run_id
    AND status IN ('queued', 'running', 'streaming', 'recovered');
END;
$$;


ALTER FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") IS 'Lightweight keep-alive pinged by the execution worker every HEARTBEAT_MS. Used by fn_claim_stale_workflow_run (Phase 7) to detect stalled runs. Only writes while the run is in a non-terminal state.';



CREATE OR REPLACE FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid" DEFAULT NULL::"uuid", "p_event_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 100, "p_offset" integer DEFAULT 0) RETURNS SETOF "agents"."v_human_fleet_logs"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT *
    FROM agents.v_human_fleet_logs
   WHERE human_lenser_id = p_human_lenser_id
     AND human_lenser_id = auth.uid()
     AND (p_run_id     IS NULL OR team_run_id = p_run_id)
     AND (p_event_type IS NULL OR event_type   = p_event_type)
   ORDER BY occurred_at DESC
   LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 100), 500))
  OFFSET GREATEST(0, COALESCE(p_offset, 0));
$$;


ALTER FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text" DEFAULT NULL::"text", "p_agent_id" "uuid" DEFAULT NULL::"uuid", "p_since" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS SETOF "agents"."v_human_fleet_runs"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT *
    FROM agents.v_human_fleet_runs
   WHERE human_lenser_id = p_human_lenser_id
     AND human_lenser_id = auth.uid()
     AND (p_status   IS NULL OR status     = p_status)
     AND (p_agent_id IS NULL OR ai_lenser_id = p_agent_id)
     AND (p_since    IS NULL OR created_at >= p_since)
   ORDER BY created_at DESC
   LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 50), 200))
  OFFSET GREATEST(0, COALESCE(p_offset, 0));
$$;


ALTER FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text" DEFAULT NULL::"text", "p_action_url" "text" DEFAULT NULL::"text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb", "p_actor_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_id UUID;
BEGIN
  -- Recipient must exist
  IF NOT EXISTS (SELECT 1 FROM lensers.profiles WHERE id = p_lenser_id) THEN
    RETURN NULL;
  END IF;

  -- Privacy gate: only runs when an actor is identified
  IF p_actor_id IS NOT NULL THEN
    IF NOT public.fn_should_send_notification(p_lenser_id, p_actor_id, p_type) THEN
      RETURN NULL;
    END IF;
  END IF;

  INSERT INTO public.notifications (lenser_id, type, title, body, action_url, metadata)
  VALUES (p_lenser_id, p_type, p_title, p_body, p_action_url, p_metadata)
  RETURNING id INTO v_id;

  RETURN v_id;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_insert_notification failed for lenser=% type=%: %',
    p_lenser_id, p_type, SQLERRM;
  RETURN NULL;
END;
$$;


ALTER FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_integrity_probe_elo_log"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation', 'pg_catalog'
    AS $$
DECLARE
  v_table_exists boolean;
  v_has_pk_on_battle_id boolean;
BEGIN
  SELECT EXISTS (
    SELECT 1 FROM pg_class c
    JOIN pg_namespace n ON n.oid = c.relnamespace
    WHERE n.nspname = 'reputation' AND c.relname = 'elo_battle_log'
  ) INTO v_table_exists;

  SELECT EXISTS (
    SELECT 1 FROM pg_constraint con
    JOIN pg_class c ON c.oid = con.conrelid
    JOIN pg_namespace n ON n.oid = c.relnamespace
    JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY (con.conkey)
    WHERE n.nspname = 'reputation'
      AND c.relname = 'elo_battle_log'
      AND con.contype = 'p'
      AND a.attname = 'battle_id'
  ) INTO v_has_pk_on_battle_id;

  RETURN jsonb_build_object(
    'table_exists', v_table_exists,
    'has_pk_on_battle_id', v_has_pk_on_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_integrity_probe_elo_log"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_integrity_probe_elo_log"() IS 'Phase P automation. Reflects the existence + PK shape of reputation.elo_battle_log so the staging integrity script can verify idempotency infrastructure. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'audit'
    AS $$
DECLARE
  v_decision_id uuid;
  v_audit_row_present boolean;
BEGIN
  -- Insert a synthetic flagged decision (AI-moderated, no human moderator).
  INSERT INTO audit.moderation_decisions
    (target_entity_schema, target_entity_table, target_entity_id,
     decision_type, reason, is_ai_moderated, ai_confidence)
  VALUES
    ('battles', 'battles', p_target_entity_id,
     'flagged', '__integrity_probe_synthetic_flag', true, 0.95)
  RETURNING id INTO v_decision_id;

  -- Verify the row landed.
  SELECT EXISTS (
    SELECT 1 FROM audit.moderation_decisions
    WHERE id = v_decision_id AND decision_type = 'flagged'
  ) INTO v_audit_row_present;

  -- Cleanup so probe rows don't accumulate.
  DELETE FROM audit.moderation_decisions WHERE id = v_decision_id;

  RETURN jsonb_build_object(
    'decision_id', v_decision_id,
    'audit_row_present', v_audit_row_present,
    'cleaned_up', true
  );
END;
$$;


ALTER FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") IS 'Phase P automation. Inserts a synthetic flagged moderation_decisions row, verifies it landed, then cleans up. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'audit'
    AS $$
DECLARE
  v_probe_id text := '__integrity_probe_' || gen_random_uuid()::text;
  v_decision_id uuid;
  v_outbox_id uuid;
  v_prior_url text := current_setting('app.moderation_webhook_url', true);
BEGIN
  -- Temporarily point the webhook URL at the listener.
  PERFORM set_config('app.moderation_webhook_url', p_target_url, true);

  -- Insert a flagged decision; the AFTER INSERT trigger enqueues into outbox.
  INSERT INTO audit.moderation_decisions
    (target_entity_schema, target_entity_table, target_entity_id,
     decision_type, reason, is_ai_moderated, ai_confidence)
  VALUES
    ('battles', 'battles', gen_random_uuid(),
     'flagged', v_probe_id, true, 0.99)
  RETURNING id INTO v_decision_id;

  -- Restore prior URL setting for the rest of the session.
  PERFORM set_config('app.moderation_webhook_url', COALESCE(v_prior_url, ''), true);

  -- Force-run the dispatcher so the listener gets the POST without waiting
  -- for the cron tick. dispatch_webhook_outbox is idempotent.
  PERFORM audit.fn_dispatch_webhook_outbox(50);

  -- Cleanup the synthetic decision.
  DELETE FROM audit.moderation_decisions WHERE id = v_decision_id;

  RETURN jsonb_build_object(
    'probe_id', v_probe_id,
    'decision_id', v_decision_id,
    'enqueued', true
  );
END;
$$;


ALTER FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") IS 'Phase P automation. Inserts a synthetic flagged decision with the listener URL set transiently, force-dispatches the outbox, cleans up. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid" DEFAULT NULL::"uuid", "p_display_name" "text" DEFAULT NULL::"text", "p_handle" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "slot" "text", "contender_type" "text", "display_name" "text", "contender_ref_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_battle    battles.battles;
  v_profile   lensers.profiles;
  v_ref_id    uuid;
  v_display   text;
BEGIN
  -- Verify caller owns the battle
  SELECT b.* INTO v_battle
    FROM battles.battles b
   WHERE b.id = p_battle_id;

  IF NOT FOUND OR v_battle.creator_lenser_id <> v_lenser_id THEN
    RAISE EXCEPTION 'invite_contender_forbidden' USING ERRCODE = '42501';
  END IF;

  -- Resolve contender: handle takes priority; fall back to ref_id
  IF p_handle IS NOT NULL THEN
    SELECT p.* INTO v_profile
      FROM lensers.profiles p
     WHERE lower(p.handle) = lower(ltrim(p_handle, '@'))
       AND p.status = 'active'
       AND p.deletion_requested_at IS NULL;

    IF NOT FOUND THEN
      RAISE EXCEPTION 'lenser_not_found' USING ERRCODE = 'P0002';
    END IF;

    v_ref_id  := v_profile.id;
    v_display := COALESCE(p_display_name, v_profile.display_name);

  ELSIF p_contender_ref_id IS NOT NULL THEN
    v_ref_id  := p_contender_ref_id;
    v_display := p_display_name;

    SELECT p.* INTO v_profile
      FROM lensers.profiles p
     WHERE p.id = p_contender_ref_id;

  ELSE
    RAISE EXCEPTION 'must provide p_contender_ref_id or p_handle' USING ERRCODE = '22023';
  END IF;

  -- Validate lenser type against battle mode
  IF v_profile.id IS NOT NULL THEN
    CASE v_battle.battle_type
      WHEN 'ai_vs_ai' THEN
        IF v_profile.type <> 'ai' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: this battle requires an AI lenser'
            USING ERRCODE = '22000';
        END IF;

      WHEN 'human_vs_human_ai_votes', 'human_vs_human_open_votes' THEN
        IF v_profile.type <> 'human' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: this battle requires a human lenser'
            USING ERRCODE = '22000';
        END IF;

      WHEN 'human_vs_ai' THEN
        IF p_slot = 'A' AND v_profile.type <> 'human' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: slot A in a human_vs_ai battle must be a human lenser'
            USING ERRCODE = '22000';
        ELSIF p_slot = 'B' AND v_profile.type <> 'ai' THEN
          RAISE EXCEPTION 'lenser_type_mismatch: slot B in a human_vs_ai battle must be an AI lenser'
            USING ERRCODE = '22000';
        END IF;

      ELSE
        -- lenser_battle, workflow_battle: any lenser type is allowed
        NULL;
    END CASE;
  END IF;

  -- Insert via CTE to avoid the ambiguous "id" reference between the
  -- RETURNS TABLE output column and the actual contenders.id table column.
  RETURN QUERY
  WITH ins AS (
    INSERT INTO battles.contenders (
      battle_id, slot, contender_type, contender_ref_id, display_name,
      entry_mode, contender_status
    ) VALUES (
      p_battle_id,
      p_slot,
      p_contender_type::battles.contender_type_enum,
      v_ref_id,
      v_display,
      'invited',
      'pending'
    )
    RETURNING *
  )
  SELECT
    ins.id,
    ins.battle_id,
    ins.slot::text,
    ins.contender_type::text,
    ins.display_name,
    ins.contender_ref_id
  FROM ins;
END;
$$;


ALTER FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") IS 'Invite a lenser as a contender. Caller must own the battle. Accepts p_contender_ref_id (UUID) or p_handle (text, @-prefix stripped) for lookup. Validates lenser.type against battle_type: ai_vs_ai requires ai, human_vs_human_* requires human, human_vs_ai enforces human on slot A and ai on slot B. Ambiguous id column fixed via CTE pattern.';



CREATE OR REPLACE FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_tool          agents.tools_registry;
  v_assignment    agents.tool_assignments;
  v_profile       agents.tool_profiles;
  v_requires      boolean := false;
  v_appr_status   text;
  v_status        text;
  v_started_at    timestamptz;
  v_id            uuid;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_tool FROM agents.tools_registry WHERE id = p_tool_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'tool not found' USING ERRCODE = 'P0002';
  END IF;
  IF v_tool.status <> 'active' THEN
    RAISE EXCEPTION 'tool % is %', v_tool.key, v_tool.status USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_assignment
    FROM agents.tool_assignments
   WHERE ai_lenser_id = p_ai_lenser_id AND tool_id = p_tool_id;
  IF NOT FOUND OR v_assignment.allowed IS DISTINCT FROM true THEN
    RAISE EXCEPTION 'tool % not assigned to agent', v_tool.key USING ERRCODE = '42501';
  END IF;

  IF v_assignment.profile_id IS NOT NULL THEN
    SELECT * INTO v_profile FROM agents.tool_profiles WHERE id = v_assignment.profile_id;
    IF FOUND AND v_profile.requires_approval THEN
      v_requires := true;
    END IF;
  END IF;

  IF v_tool.requires_approval OR v_tool.egress_class = 'write' THEN
    v_requires := true;
  END IF;

  IF v_requires THEN
    v_appr_status := 'pending';
    v_status      := 'pending';
    v_started_at  := NULL;
  ELSE
    v_appr_status := 'not_required';
    v_status      := 'running';
    v_started_at  := now();
  END IF;

  INSERT INTO agents.tool_invocations (
    team_run_id, agent_run_step_id, tool_id, ai_lenser_id,
    input, status, approval_status, approval_required, started_at
  )
  VALUES (
    p_team_run_id, p_agent_run_step_id, p_tool_id, p_ai_lenser_id,
    COALESCE(p_input, '{}'::jsonb), v_status, v_appr_status, v_requires, v_started_at
  )
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1
    FROM   lensers.relationships
    WHERE  source_profile_id = p_recipient_id
      AND  target_profile_id = p_actor_id
      AND  status = 'blocked'
      AND  removed_at IS NULL
  );
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_is_notification_blocked failed: %', SQLERRM;
  RETURN false;  -- fail-open: never suppress by accident
END;
$$;


ALTER FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  RETURN COALESCE(
    (SELECT NOT enabled
     FROM   public.notification_preferences
     WHERE  lenser_id         = p_lenser_id
       AND  notification_type = p_type),
    false
  );
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_is_notification_muted failed: %', SQLERRM;
  RETURN false;  -- fail-open
END;
$$;


ALTER FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_is_super_admin"() RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  SELECT COALESCE(
    (SELECT is_super_admin FROM lensers.profiles WHERE id = auth.uid() LIMIT 1),
    FALSE
  );
$$;


ALTER FUNCTION "public"."fn_is_super_admin"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_journey_state_get"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_state JSONB;
BEGIN
  SELECT journey_state
    INTO v_state
    FROM lensers.profiles
   WHERE user_id = auth.uid();

  RETURN COALESCE(v_state, '{}'::JSONB);
END;
$$;


ALTER FUNCTION "public"."fn_journey_state_get"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_journey_state_get"() IS 'Returns the journey_state JSONB for the calling lenser. Safe to call before any step is marked.';



CREATE OR REPLACE FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean DEFAULT true) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  UPDATE lensers.profiles
     SET journey_state = journey_state || jsonb_build_object(p_step, p_done),
         updated_at    = NOW()
   WHERE user_id = auth.uid();

  -- Silently no-op if the profile row doesn't exist yet; the CLI handles null.
  IF NOT FOUND THEN
    NULL;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) IS 'Sets or clears a journey step key for the calling lenser. Idempotent; no-ops on missing profile.';



CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid" DEFAULT NULL::"uuid", "p_reason" "text" DEFAULT 'emergency stop'::"text", "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_operator_id UUID;
  v_switch_id   UUID;
BEGIN
  -- Admin-only
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'kill_switch_activate_forbidden: admin role required';
  END IF;

  SELECT id INTO v_operator_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  IF v_operator_id IS NULL THEN
    RAISE EXCEPTION 'kill_switch_activate_no_profile: lenser profile not found';
  END IF;

  INSERT INTO admin.kill_switches (scope, target_id, operator_id, reason, expires_at)
  VALUES (p_scope, p_target_id, v_operator_id, p_reason, p_expires_at)
  RETURNING id INTO v_switch_id;

  -- Audit trail
  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'kill_switch.activated',
    'lenser',
    v_operator_id,
    'warn',
    jsonb_build_object(
      'switch_id', v_switch_id,
      'scope',     p_scope,
      'target_id', p_target_id,
      'reason',    p_reason
    )
  );

  RETURN v_switch_id;
END;
$$;


ALTER FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid" DEFAULT NULL::"uuid") RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'admin', 'public'
    AS $$
  SELECT EXISTS (
    SELECT 1
    FROM   admin.kill_switches ks
    WHERE  ks.lifted_at IS NULL
    AND    (ks.expires_at IS NULL OR ks.expires_at > now())
    AND    (
             -- System-wide switch halts everything
             ks.scope = 'system'
             OR
             -- Scope + target match
             (ks.scope = p_scope AND ks.target_id = p_target_id)
           )
  );
$$;


ALTER FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_operator_id UUID;
  v_switch      admin.kill_switches%ROWTYPE;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'kill_switch_lift_forbidden: admin role required';
  END IF;

  SELECT * INTO v_switch FROM admin.kill_switches WHERE id = p_switch_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'kill_switch_not_found: %', p_switch_id;
  END IF;

  IF v_switch.lifted_at IS NOT NULL THEN
    RAISE EXCEPTION 'kill_switch_already_lifted: %', p_switch_id;
  END IF;

  UPDATE admin.kill_switches SET lifted_at = now() WHERE id = p_switch_id;

  SELECT id INTO v_operator_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'kill_switch.lifted',
    'lenser',
    v_operator_id,
    'info',
    jsonb_build_object(
      'switch_id',     p_switch_id,
      'original_scope', v_switch.scope,
      'target_id',     v_switch.target_id
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_kill_switch_list"() RETURNS TABLE("id" "uuid", "scope" "text", "target_id" "uuid", "reason" "text", "expires_at" timestamp with time zone, "activated_at" timestamp with time zone, "lifted_at" timestamp with time zone, "operator_handle" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
  SELECT
    ks.id,
    ks.scope,
    ks.target_id,
    ks.reason,
    ks.expires_at,
    ks.activated_at,
    ks.lifted_at,
    p.handle AS operator_handle
  FROM  admin.kill_switches ks
  LEFT  JOIN lensers.profiles p ON p.id = ks.operator_id
  ORDER BY ks.activated_at DESC;
$$;


ALTER FUNCTION "public"."fn_kill_switch_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean DEFAULT false) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lenses'
    AS $$
DECLARE
  v_lens_id UUID;
BEGIN
  BEGIN
    v_lens_id := p_slug::uuid;
  EXCEPTION WHEN invalid_text_representation THEN
    SELECT l.id INTO v_lens_id
    FROM lenses.lenses l
    LEFT JOIN content.entity_translations et
      ON et.entity_id = l.id
     AND et.entity_type = 'lens'::content.entity_type_enum
     AND et.is_original = TRUE
    WHERE lower(replace(COALESCE(et.title, l.id::text), ' ', '-')) = lower(p_slug)
       OR lower(COALESCE(et.title, '')) = lower(p_slug)
    ORDER BY l.created_at DESC
    LIMIT 1;
  END;

  IF v_lens_id IS NULL THEN
    RAISE EXCEPTION 'lens_not_found: %', p_slug USING ERRCODE = 'P0002';
  END IF;

  RETURN public.fn_artifact_pin('lens', v_lens_id, NOT COALESCE(p_unstar, FALSE));
END;
$$;


ALTER FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text" DEFAULT ''::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_row lensers.profiles;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  INSERT INTO lensers.profiles (user_id, handle, display_name, bio, headline, onboarding_step)
  VALUES (v_uid, p_handle, p_display_name, COALESCE(p_bio, ''), NULL, 1)
  RETURNING * INTO v_row;

  RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  v_result := public.fn_request_follow(p_following_id);
  -- Return old format for backward compat
  RETURN jsonb_build_object('following', true);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_active_profile"() RETURNS SETOF "lensers"."profiles"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT p.*
  FROM lensers.profiles p
  WHERE p.id = lensers.get_auth_lenser_id()
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_lensers_get_active_profile"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_get_active_profile"() IS 'Returns the active lenser workspace profile for the authenticated user. Unlike user_id-based lookups this follows preferences.active_lenser_id when an owned AI workspace is selected.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_current_id"() RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT lp.id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
    AND lp.status = 'active'
    AND lp.deletion_requested_at IS NULL
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_lensers_get_current_id"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text" DEFAULT 'following'::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "is_following" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT
    lp.id,
    lp.handle,
    lp.display_name,
    lp.avatar_url,
    EXISTS (
      SELECT 1 FROM lensers.relationships r2
      JOIN lensers.profiles me ON me.user_id = auth.uid()
      WHERE r2.source_profile_id = me.id
        AND r2.target_profile_id = lp.id
        AND r2.status = 'accepted'
    ) AS is_following
  FROM lensers.relationships r
  JOIN lensers.profiles lp ON lp.id = CASE
    WHEN p_type = 'followers' THEN r.source_profile_id
    ELSE r.target_profile_id
  END
  WHERE CASE
    WHEN p_type = 'followers' THEN r.target_profile_id = p_lenser_id
    ELSE r.source_profile_id = p_lenser_id
  END
    AND r.status = 'accepted'
    AND lp.status = 'active'::lensers.lenser_status
    AND lp.deletion_requested_at IS NULL
  ORDER BY r.accepted_at DESC
  LIMIT LEAST(p_limit, 100)
  OFFSET GREATEST(p_offset, 0);
$$;


ALTER FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_fresh_profile"() RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "headline" "text", "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  select 
    id,
    handle,
    display_name,
    avatar_url,
    headline,
    updated_at
  from lensers.profiles
  where user_id = auth.uid();
$$;


ALTER FUNCTION "public"."fn_lensers_get_fresh_profile"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  select p.is_in_waiting_list
  from lensers.profiles p
  where p.user_id = auth.uid()
  limit 1;
$$;


ALTER FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text" DEFAULT 'all_time'::"text", "p_limit" integer DEFAULT 20) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_xp" bigint, "current_level" integer, "lenser_score" double precision, "rank" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'xp', 'lenses', 'content'
    AS $$
WITH ranked AS (
  SELECT
    lp.id            AS lenser_id,
    lp.handle,
    lp.display_name,
    lp.avatar_url,
    COALESCE(xt.total_xp,      0)::bigint AS total_xp,
    COALESCE(xt.current_level, 1)          AS current_level,
    ROW_NUMBER() OVER (ORDER BY COALESCE(xt.total_xp, 0) DESC) AS rank
  FROM lensers.profiles lp
  LEFT JOIN (
    SELECT t.lenser_id,
           SUM(t.total_xp)       AS total_xp,
           MAX(t.current_level)  AS current_level
    FROM xp.totals t
    GROUP BY t.lenser_id
  ) xt ON xt.lenser_id = lp.id
  WHERE lp.status              = 'active'
    AND lp.visibility          = 'public'
    AND lp.deletion_requested_at IS NULL
    AND CASE p_period
          WHEN 'weekly'  THEN lp.last_active_at > now() - interval '7 days'
          WHEN 'monthly' THEN lp.last_active_at > now() - interval '30 days'
          ELSE true
        END
  ORDER BY COALESCE(xt.total_xp, 0) DESC
  -- clamp: at least 1, at most 100; negative or zero inputs become 1
  LIMIT GREATEST(1, LEAST(COALESCE(p_limit, 20), 100))
)
SELECT
  r.lenser_id,
  r.handle,
  r.display_name,
  r.avatar_url,
  r.total_xp,
  r.current_level,
  (
    0.7 * log(GREATEST(1.0, r.total_xp::double precision))
    + 0.3 * log(GREATEST(1.0, COALESCE(re7.cnt, 0)::double precision))
  ) AS lenser_score,
  r.rank
FROM ranked r
LEFT JOIN LATERAL (
  SELECT count(*) AS cnt
  FROM (
    SELECT 1
    FROM content.reactions tr
    JOIN content.threads t ON t.id = tr.entity_id AND t.lenser_id = r.lenser_id
    WHERE tr.entity_type = 'thread'
      AND tr.created_at > now() - interval '7 days'
    UNION ALL
    SELECT 1
    FROM content.reactions pr
    JOIN lenses.lenses pt ON pt.id = pr.entity_id AND pt.lenser_id = r.lenser_id
    WHERE pr.entity_type = 'lens'
      AND pr.created_at > now() - interval '7 days'
  ) x
) re7 ON true
ORDER BY r.rank;
$$;


ALTER FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_preferences"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
    v_uid  uuid;
    v_row  lensers.preferences;
BEGIN
    v_uid := auth.uid();
    IF v_uid IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT p.*
      INTO v_row
      FROM lensers.preferences p
      JOIN lensers.profiles    pr ON pr.id = p.lenser_id
     WHERE pr.user_id = v_uid;

    IF NOT FOUND THEN
        RETURN '{}'::jsonb;
    END IF;

    RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_get_preferences"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_get_preferences"() IS 'Returns the authenticated user''s full preferences row as jsonb. Reads from lensers.preferences table (previously read from lensers.profiles.preferences jsonb). Updated in migration 20260322000059.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth', 'analytics', 'xp'
    AS $$
DECLARE
  v_viewer_uid uuid;
  v_subject RECORD;
  v_prefs lensers.preferences;
  v_access lensers.profile_access_level;
  v_viewer_profile_id uuid;
  v_rel_state jsonb;
  v_result jsonb;
  v_xp RECORD;
  v_min_xp bigint;
  v_max_xp bigint;
  v_stats RECORD;
  v_join RECORD;
  v_is_owner boolean := false;
  v_completion_score integer;
  v_activity jsonb;
BEGIN
  v_viewer_uid := auth.uid();

  -- Fetch subject profile (without visibility filter)
  SELECT p.id, p.handle, p.display_name, p.avatar_url, p.banner_url,
         p.bio, p.headline, p.status, p.visibility, p.created_at,
         p.type,
         p.user_id, p.deletion_requested_at, p.deletion_deadline_at,
         p.location, p.website_url
  INTO v_subject
  FROM lensers.profiles p
  WHERE p.handle = p_handle;

  IF NOT FOUND THEN
    RETURN jsonb_build_object(
      'route_state', 'UNAVAILABLE_PROFILE',
      'access_reason', 'not_found',
      'relationship_state', null,
      'profile', null
    );
  END IF;

  -- Fetch preferences row (1:1 with profile)
  SELECT * INTO v_prefs
  FROM lensers.preferences
  WHERE lenser_id = v_subject.id;

  -- Resolve access level
  v_access := lensers.fn_can_view_profile(v_viewer_uid, v_subject.id);

  -- Get viewer profile id for relationship state
  IF v_viewer_uid IS NOT NULL THEN
    SELECT p.id INTO v_viewer_profile_id
    FROM lensers.profiles p
    WHERE p.user_id = v_viewer_uid;
  END IF;

  -- Determine ownership
  v_is_owner := (v_viewer_uid IS NOT NULL AND v_viewer_uid = v_subject.user_id);

  -- Build relationship state
  IF v_viewer_profile_id IS NOT NULL AND v_viewer_profile_id <> v_subject.id THEN
    v_rel_state := lensers.fn_relationship_state(v_viewer_profile_id, v_subject.id);
  ELSE
    v_rel_state := null;
  END IF;

  -- UNAVAILABLE → minimal response
  IF v_access = 'UNAVAILABLE_PROFILE' THEN
    RETURN jsonb_build_object(
      'route_state', 'UNAVAILABLE_PROFILE',
      'access_reason', 'unavailable',
      'relationship_state', v_rel_state,
      'profile', null
    );
  END IF;

  -- Fetch XP, stats, join info for full/restricted/recovery
  SELECT t.total_xp, t.current_level, t.app_id INTO v_xp
  FROM xp.totals t WHERE t.lenser_id = v_subject.id;

  IF v_xp.app_id IS NOT NULL AND v_xp.current_level IS NOT NULL THEN
    SELECT lv.min_total_xp, lv.max_total_xp INTO v_min_xp, v_max_xp
    FROM xp.levels lv
    WHERE lv.app_id = v_xp.app_id AND lv.level = v_xp.current_level;
  END IF;

  SELECT s.thread_count, s.lens_count, s.follower_count, s.following_count, s.mutuals_count
  INTO v_stats
  FROM analytics.lenser_stats s WHERE s.lenser_id = v_subject.id;

  SELECT jl.join_order, jl.joined_at INTO v_join
  FROM analytics.lenser_join_log jl WHERE jl.lenser_id = v_subject.id;

  -- RESTRICTED → limited fields (no activity/completion)
  IF v_access = 'RESTRICTED_PROFILE' THEN
    RETURN jsonb_build_object(
      'route_state', 'RESTRICTED_PROFILE',
      'access_reason', 'private_not_following',
      'relationship_state', v_rel_state,
      'profile', jsonb_build_object(
        'id', v_subject.id,
        'handle', v_subject.handle,
        'display_name', v_subject.display_name,
        'avatar_url', v_subject.avatar_url,
        'type', v_subject.type,
        'visibility', v_subject.visibility,
        'total_xp', v_xp.total_xp,
        'current_level', v_xp.current_level,
        'follower_count', COALESCE(v_stats.follower_count, 0),
        'following_count', COALESCE(v_stats.following_count, 0),
        'join_order', v_join.join_order,
        'created_at', v_subject.created_at
      )
    );
  END IF;

  -- ── Completion score (owner only) ──────────────────────────────────────────
  IF v_is_owner THEN
    v_completion_score := 0;

    IF v_subject.bio IS NOT NULL AND length(trim(v_subject.bio)) > 0 THEN
      v_completion_score := v_completion_score + 25;
    END IF;
    IF v_subject.avatar_url IS NOT NULL AND length(trim(v_subject.avatar_url)) > 0 THEN
      v_completion_score := v_completion_score + 25;
    END IF;
    IF v_subject.location IS NOT NULL AND length(trim(v_subject.location)) > 0 THEN
      v_completion_score := v_completion_score + 15;
    END IF;
    IF v_subject.website_url IS NOT NULL AND length(trim(v_subject.website_url)) > 0 THEN
      v_completion_score := v_completion_score + 15;
    END IF;

    -- Battle participation
    IF EXISTS (
      SELECT 1 FROM battles.contenders bc
      JOIN battles.battles b ON b.id = bc.battle_id
      WHERE bc.contender_ref_id = v_subject.id
        AND b.status <> 'draft'::battles.battle_status_enum
    ) THEN
      v_completion_score := v_completion_score + 10;
    END IF;

    -- XP earned
    IF COALESCE((SELECT SUM(xp) FROM xp.events WHERE lenser_id = v_subject.id), 0) > 0 THEN
      v_completion_score := v_completion_score + 10;
    END IF;

    v_completion_score := LEAST(v_completion_score, 100);
  END IF;

  -- ── Activity timeline (365 days) ──────────────────────────────────────────
  SELECT COALESCE(jsonb_agg(
    jsonb_build_object('date', sub.date, 'count', sub.count)
    ORDER BY sub.date
  ), '[]'::jsonb)
  INTO v_activity
  FROM (
    WITH calendar AS (
      SELECT (current_date - s.i)::date AS day
      FROM generate_series(0, 364) AS s(i)
    ),
    events AS (
      -- Lens creations
      SELECT date_trunc('day', l.created_at)::date AS day
      FROM   lenses.lenses l
      WHERE  l.lenser_id = v_subject.id
        AND  l.visibility IN ('public', 'community')
        AND  l.status = 'published'
        AND  l.created_at >= (current_date - interval '364 days')

      UNION ALL

      -- Thread creations
      SELECT date_trunc('day', t.created_at)::date AS day
      FROM   content.threads t
      WHERE  t.lenser_id = v_subject.id
        AND  t.visibility IN ('public', 'community')
        AND  t.status = 'published'
        AND  t.created_at >= (current_date - interval '364 days')

      UNION ALL

      -- Reactions (skipped when hide_actions = true)
      SELECT date_trunc('day', rx.created_at)::date AS day
      FROM   content.reactions rx
      WHERE  rx.lenser_id = v_subject.id
        AND  COALESCE(v_prefs.hide_actions, false) = false
        AND  rx.created_at >= (current_date - interval '364 days')
    ),
    daily_counts AS (
      SELECT day, count(*) AS cnt
      FROM   events
      GROUP BY day
    )
    SELECT to_char(c.day, 'YYYY-MM-DD') AS date,
           COALESCE(dc.cnt, 0)          AS count
    FROM   calendar c
    LEFT JOIN daily_counts dc ON dc.day = c.day
  ) sub;

  -- Determine access reason
  DECLARE
    v_reason text;
  BEGIN
    IF v_access = 'OWNER_RECOVERY_PROFILE' THEN
      v_reason := 'owner_recovery';
    ELSIF v_subject.visibility = 'private' THEN
      v_reason := 'private_following';
    ELSE
      v_reason := 'public';
    END IF;

    -- FULL / OWNER_RECOVERY → all fields
    v_result := jsonb_build_object(
      'route_state', v_access::text,
      'access_reason', v_reason,
      'relationship_state', v_rel_state,
      'completion_score', v_completion_score,
      'activity_timeline', v_activity,
      'profile', jsonb_build_object(
        'id', v_subject.id,
        'handle', v_subject.handle,
        'display_name', v_subject.display_name,
        'avatar_url', v_subject.avatar_url,
        'banner_url', v_subject.banner_url,
        'bio', v_subject.bio,
        'headline', v_subject.headline,
        'status', v_subject.status,
        'type', v_subject.type,
        'visibility', v_subject.visibility,
        'created_at', v_subject.created_at,
        'total_xp', v_xp.total_xp,
        'current_level', v_xp.current_level,
        'min_xp', v_min_xp,
        'max_xp', v_max_xp,
        'app_id', v_xp.app_id,
        'thread_count', COALESCE(v_stats.thread_count, 0),
        'lens_count', COALESCE(v_stats.lens_count, 0),
        'follower_count', COALESCE(v_stats.follower_count, 0),
        'following_count', COALESCE(v_stats.following_count, 0),
        'join_order', v_join.join_order,
        'joined_at', v_join.joined_at,
        'hide_actions', COALESCE(v_prefs.hide_actions, false),
        'content_visibility', COALESCE(v_prefs.content_visibility, 'public'),
        'deletion_deadline_at', v_subject.deletion_deadline_at
      )
    );

    RETURN v_result;
  END;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_verdict     text;
  v_result      jsonb;
  v_route_state text;
BEGIN
  -- ── Governance gate ────────────────────────────────────────────────────────
  -- Validate the incoming handle against the identity_gov registry before
  -- touching the profile table. A 'deny' verdict returns null — same as the
  -- not-found or private-profile path — preventing namespace enumeration.
  SELECT verdict
  INTO   v_verdict
  FROM   identity_gov.fn_validate_handle(p_handle)
  LIMIT  1;

  IF v_verdict = 'deny' THEN
    RETURN null;
  END IF;

  -- ── Normal profile lookup ─────────────────────────────────────────────────
  v_result      := public.fn_lensers_get_profile(p_handle);
  v_route_state := v_result->>'route_state';

  IF v_route_state = 'FULL_PROFILE' THEN
    RETURN v_result->'profile';
  ELSE
    RETURN null;
  END IF;

EXCEPTION WHEN OTHERS THEN
  -- Fail safe: governance error or profile error returns null, not a 500.
  RETURN null;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") IS 'Public profile lookup with identity_gov namespace guard. Returns null for denied handles (reserved, impersonation) and for non-FULL_PROFILE route states (private, deactivated, deleted). Callers cannot distinguish denied-by-governance from not-found.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 10) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_xp" bigint, "current_level" integer, "lenser_score" double precision, "tag_overlap_score" double precision)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'lenses', 'content', 'xp'
    AS $$
WITH
  interest_tags AS (
    SELECT tag_id FROM lensers.tag_follows WHERE lenser_id = p_lenser_id
  ),
  already_following AS (
    SELECT target_profile_id AS following_id
    FROM lensers.relationships
    WHERE source_profile_id = p_lenser_id
      AND status = 'accepted'
  ),
  thread_author_tags AS (
    SELECT
      t.lenser_id,
      COUNT(DISTINCT it.tag_id)::float
        / GREATEST((SELECT COUNT(*) FROM interest_tags), 1) AS tag_score
    FROM content.threads    t
    JOIN content.tag_map    tm ON tm.entity_id = t.id AND tm.entity_type = 'thread'
    JOIN interest_tags      it ON it.tag_id = tm.tag_id
    WHERE t.visibility = 'public'::content.visibility_enum
    GROUP BY t.lenser_id
  ),
  prompt_author_tags AS (
    SELECT
      pt.lenser_id,
      COUNT(DISTINCT it.tag_id)::float
        / GREATEST((SELECT COUNT(*) FROM interest_tags), 1) AS tag_score
    FROM lenses.lenses pt
    JOIN content.tag_map  tm ON tm.entity_id = pt.id AND tm.entity_type = 'lens'
    JOIN interest_tags    it ON it.tag_id = tm.tag_id
    WHERE pt.visibility = 'public'::content.visibility_enum
    GROUP BY pt.lenser_id
  ),
  author_tag_agg AS (
    SELECT lenser_id, AVG(tag_score) AS tag_overlap_score
    FROM (
      SELECT lenser_id, tag_score FROM thread_author_tags
      UNION ALL
      SELECT lenser_id, tag_score FROM prompt_author_tags
    ) combined
    GROUP BY lenser_id
  ),
  tag_pool AS (
    SELECT
      lp.id          AS lenser_id,
      lp.handle,
      lp.display_name,
      lp.avatar_url,
      COALESCE(xt.total_xp, 0)::bigint    AS total_xp,
      COALESCE(xt.current_level, 1)        AS current_level,
      COALESCE(agg.tag_overlap_score, 0.0) AS tag_overlap_score
    FROM author_tag_agg agg
    JOIN lensers.profiles lp ON lp.id = agg.lenser_id
    LEFT JOIN (
      SELECT t.lenser_id, SUM(t.total_xp) AS total_xp, MAX(t.current_level) AS current_level
      FROM xp.totals t GROUP BY t.lenser_id
    ) xt ON xt.lenser_id = lp.id
    WHERE lp.status                = 'active'
      AND lp.visibility            = 'public'
      AND lp.onboarding_step       = 2
      AND lp.deletion_requested_at IS NULL
      AND lp.id <> p_lenser_id
      AND lp.id NOT IN (SELECT following_id FROM already_following)
  ),
  xp_pool AS (
    SELECT
      lp.id          AS lenser_id,
      lp.handle,
      lp.display_name,
      lp.avatar_url,
      COALESCE(xt.total_xp, 0)::bigint AS total_xp,
      COALESCE(xt.current_level, 1)     AS current_level,
      0.0::double precision             AS tag_overlap_score
    FROM lensers.profiles lp
    LEFT JOIN (
      SELECT t.lenser_id, SUM(t.total_xp) AS total_xp, MAX(t.current_level) AS current_level
      FROM xp.totals t GROUP BY t.lenser_id
    ) xt ON xt.lenser_id = lp.id
    WHERE lp.status                = 'active'
      AND lp.visibility            = 'public'
      AND lp.onboarding_step       = 2
      AND lp.deletion_requested_at IS NULL
      AND lp.id <> p_lenser_id
      AND lp.id NOT IN (SELECT following_id FROM already_following)
  ),
  chosen AS (
    SELECT * FROM tag_pool
    WHERE (SELECT COUNT(*) FROM tag_pool) > 0
    UNION ALL
    SELECT * FROM xp_pool
    WHERE (SELECT COUNT(*) FROM tag_pool) = 0
  ),
  top_candidates AS (
    SELECT * FROM chosen
    ORDER BY tag_overlap_score DESC, total_xp DESC
    LIMIT LEAST(p_limit, 50) * 2
  )
SELECT
  tc.lenser_id,
  tc.handle,
  tc.display_name,
  tc.avatar_url,
  tc.total_xp,
  tc.current_level,
  (
    0.7 * log(GREATEST(1.0, tc.total_xp::double precision))
    + 0.3 * log(GREATEST(1.0, COALESCE(re7.cnt, 0)::double precision))
  ) AS lenser_score,
  tc.tag_overlap_score
FROM top_candidates tc
LEFT JOIN LATERAL (
  SELECT count(*) AS cnt FROM (
    SELECT 1
    FROM content.reactions tr
    JOIN content.threads t ON t.id = tr.entity_id AND t.lenser_id = tc.lenser_id
    WHERE tr.entity_type = 'thread'
      AND tr.created_at > now() - interval '7 days'
    UNION ALL
    SELECT 1
    FROM content.reactions pr
    JOIN lenses.lenses pt ON pt.id = pr.entity_id AND pt.lenser_id = tc.lenser_id
    WHERE pr.entity_type = 'lens'
      AND pr.created_at > now() - interval '7 days'
  ) x
) re7 ON true
ORDER BY (
  0.60 * tc.tag_overlap_score
  + 0.40 * LEAST(
    (0.7 * log(GREATEST(1.0, tc.total_xp::double precision))
     + 0.3 * log(GREATEST(1.0, COALESCE(re7.cnt, 0)::double precision)))
    / 5.0, 1.0)
) DESC
LIMIT LEAST(p_limit, 50);
$$;


ALTER FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer DEFAULT 10) RETURNS TABLE("lenser_id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "total_xp" bigint, "current_level" integer, "lenser_score" double precision)
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'lensers', 'content', 'xp'
    AS $$
  SELECT
    lenser_id,
    handle,
    display_name,
    avatar_url,
    total_xp,
    current_level,
    lenser_score
  FROM lensers.vw_lensers_score
  ORDER BY lenser_score DESC
  LIMIT LEAST(p_limit, 50);
$$;


ALTER FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  SELECT EXISTS (
    SELECT 1
    FROM lensers.relationships r
    JOIN lensers.profiles p ON p.id = r.source_profile_id AND p.user_id = auth.uid()
    WHERE r.target_profile_id = p_target_id
      AND r.status = 'accepted'
  );
$$;


ALTER FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_list"("p_type" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "bio" "text", "type" "lensers"."lenser_type", "ai_model_id" "uuid", "created_at" timestamp with time zone, "engagement" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'analytics', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    p.id,
    p.handle,
    p.display_name,
    p.avatar_url,
    p.bio,
    p.type,
    p.ai_model_id,
    p.created_at,
    CASE
      WHEN s.lenser_id IS NOT NULL THEN to_jsonb(s)
      ELSE NULL::jsonb
    END AS engagement
  FROM lensers.profiles p
  LEFT JOIN analytics.lenser_stats s ON s.lenser_id = p.id
  WHERE
    p.status              = 'active'
    AND p.deletion_requested_at IS NULL
    AND p.onboarding_step >= 1
    AND p.visibility      = 'public'
    AND (p_type IS NULL OR p.type = p_type::lensers.lenser_type)
  ORDER BY p.created_at DESC
  LIMIT  LEAST(p_limit, 100)
  OFFSET GREATEST(p_offset, 0);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_request_deletion"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.profiles
  SET
    deletion_requested_at = NOW(),
    updated_at = NOW()
  WHERE user_id = v_uid
    AND status = 'active'
    AND deletion_requested_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Active Lenser profile not found or deletion already requested';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_request_deletion"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
  rec jsonb;
  v_id uuid;
  v_platform lensers.lenser_social_platform;
  v_url text;
  v_label text;
  v_incoming_ids uuid[];
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid()
    AND status = 'active'
    AND deletion_requested_at IS NULL;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Lenser profile not found';
  END IF;

  -- Collect incoming IDs
  SELECT array_agg((elem->>'id')::uuid)
  INTO v_incoming_ids
  FROM jsonb_array_elements(p_links) elem
  WHERE elem ? 'id';

  -- Delete missing
  DELETE FROM lensers.social_links
  WHERE lenser_id = v_lenser_id
    AND (v_incoming_ids IS NULL OR NOT (id = ANY (v_incoming_ids)));

  -- Upsert loop
  FOR rec IN SELECT * FROM jsonb_array_elements(p_links)
  LOOP
    v_id := COALESCE((rec->>'id')::uuid, gen_random_uuid());
    v_platform := (rec->>'platform')::lensers.lenser_social_platform;
    v_url := rec->>'url';
    v_label := rec->>'label';

    INSERT INTO lensers.social_links (id, lenser_id, platform, url, label)
    VALUES (v_id, v_lenser_id, v_platform, v_url, v_label)
    ON CONFLICT (id) DO UPDATE
      SET platform = EXCLUDED.platform,
          url      = EXCLUDED.url,
          label    = EXCLUDED.label;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public', 'auth'
    AS $$
BEGIN
  IF auth.uid() IS NULL THEN
    RAISE EXCEPTION 'not_authenticated';
  END IF;

  IF NOT p_kvkk_approved THEN
    RAISE EXCEPTION 'kvkk_not_approved';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM lensers.profiles WHERE user_id = auth.uid()
  ) THEN
    RAISE EXCEPTION 'not_a_lenser';
  END IF;

  UPDATE lensers.profiles
  SET is_in_waiting_list = NOT is_in_waiting_list
  WHERE user_id = auth.uid();
END;
$$;


ALTER FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  v_result := public.fn_remove_follow(p_following_id);
  RETURN jsonb_build_object('following', false);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'ai', 'auth'
    AS $$
DECLARE
    v_uid              uuid;
    v_lenser_id        uuid;
    v_active_lenser_id uuid;
    v_selected_key_id  uuid;
    v_key_owner        uuid;
    v_row              lensers.preferences;
BEGIN
    v_uid := auth.uid();
    IF v_uid IS NULL THEN
        RAISE EXCEPTION 'Authentication required';
    END IF;

    SELECT id INTO v_lenser_id
      FROM lensers.profiles
     WHERE user_id = v_uid
       AND type    = 'human'
     LIMIT 1;

    IF v_lenser_id IS NULL THEN
        RAISE EXCEPTION 'Profile not found';
    END IF;

    -- Resolve the active workspace context; may differ from v_lenser_id when
    -- the user has switched into an AI agent workspace.
    v_active_lenser_id := lensers.get_auth_lenser_id();

    -- Validate selected_api_key_id ownership when provided
    IF p_data ? 'selected_api_key_id' AND p_data->>'selected_api_key_id' IS NOT NULL THEN
        v_selected_key_id := (p_data->>'selected_api_key_id')::uuid;

        SELECT lenser_id INTO v_key_owner
          FROM ai.keys
         WHERE id = v_selected_key_id
           AND is_active = true;

        IF NOT FOUND THEN
            RAISE EXCEPTION 'API key not found or is not active'
                USING ERRCODE = 'P0002';
        END IF;

        -- Allow keys belonging to the human profile OR the currently active
        -- workspace (e.g. an AI agent the user has switched into).
        IF v_key_owner <> v_lenser_id AND v_key_owner <> v_active_lenser_id THEN
            RAISE EXCEPTION 'API key does not belong to this lenser'
                USING ERRCODE = 'P0003';
        END IF;
    END IF;

    UPDATE lensers.preferences
    SET
        language              = CASE WHEN p_data ? 'language'              THEN p_data->>'language'                                              ELSE language              END,
        theme                 = CASE WHEN p_data ? 'theme'                 THEN (p_data->>'theme')::text                                         ELSE theme                 END,
        notifications         = CASE WHEN p_data ? 'notifications'         THEN p_data->'notifications'                                          ELSE notifications         END,
        sidebar               = CASE WHEN p_data ? 'sidebar'               THEN p_data->'sidebar'                                                ELSE sidebar               END,
        content_visibility    = CASE WHEN p_data ? 'content_visibility'    THEN p_data->>'content_visibility'                                     ELSE content_visibility    END,
        email_digest          = CASE WHEN p_data ? 'email_digest'          THEN (p_data->>'email_digest')::boolean                               ELSE email_digest          END,
        ai_provider_key       = CASE WHEN p_data ? 'ai_provider_key'       THEN p_data->>'ai_provider_key'                                       ELSE ai_provider_key       END,
        ai_model_key          = CASE WHEN p_data ? 'ai_model_key'          THEN p_data->>'ai_model_key'                                          ELSE ai_model_key          END,
        ai_persona            = CASE WHEN p_data ? 'ai_persona'            THEN p_data->>'ai_persona'                                            ELSE ai_persona            END,
        ai_ruleset            = CASE WHEN p_data ? 'ai_ruleset'            THEN p_data->'ai_ruleset'                                             ELSE ai_ruleset            END,
        wallet_mode           = CASE WHEN p_data ? 'wallet_mode'           THEN (p_data->>'wallet_mode')::lensers.wallet_mode_enum               ELSE wallet_mode           END,
        ai_data_usage         = CASE WHEN p_data ? 'ai_data_usage'         THEN (p_data->>'ai_data_usage')::boolean                              ELSE ai_data_usage         END,
        hide_actions          = CASE WHEN p_data ? 'hide_actions'          THEN (p_data->>'hide_actions')::boolean                               ELSE hide_actions          END,
        cron_config           = CASE WHEN p_data ? 'cron_config'           THEN p_data->'cron_config'                                            ELSE cron_config           END,
        selected_api_key_id   = CASE
                                    WHEN p_data ? 'selected_api_key_id' AND p_data->>'selected_api_key_id' IS NULL
                                        THEN NULL
                                    WHEN p_data ? 'selected_api_key_id'
                                        THEN (p_data->>'selected_api_key_id')::uuid
                                    ELSE selected_api_key_id
                                END,
        updated_at            = now()
    WHERE lenser_id = v_lenser_id
    RETURNING * INTO v_row;

    IF NOT FOUND THEN
        -- Auto-create missing row for legacy profiles, then retry
        INSERT INTO lensers.preferences (lenser_id)
        VALUES (v_lenser_id)
        ON CONFLICT (lenser_id) DO NOTHING;

        UPDATE lensers.preferences
        SET
            language              = CASE WHEN p_data ? 'language'              THEN p_data->>'language'           ELSE language              END,
            theme                 = CASE WHEN p_data ? 'theme'                 THEN p_data->>'theme'              ELSE theme                 END,
            notifications         = CASE WHEN p_data ? 'notifications'         THEN p_data->'notifications'       ELSE notifications         END,
            sidebar               = CASE WHEN p_data ? 'sidebar'               THEN p_data->'sidebar'             ELSE sidebar               END,
            content_visibility    = CASE WHEN p_data ? 'content_visibility'    THEN p_data->>'content_visibility' ELSE content_visibility    END,
            email_digest          = CASE WHEN p_data ? 'email_digest'          THEN (p_data->>'email_digest')::boolean ELSE email_digest     END,
            ai_provider_key       = CASE WHEN p_data ? 'ai_provider_key'       THEN p_data->>'ai_provider_key'    ELSE ai_provider_key       END,
            ai_model_key          = CASE WHEN p_data ? 'ai_model_key'          THEN p_data->>'ai_model_key'       ELSE ai_model_key          END,
            ai_persona            = CASE WHEN p_data ? 'ai_persona'            THEN p_data->>'ai_persona'         ELSE ai_persona            END,
            ai_ruleset            = CASE WHEN p_data ? 'ai_ruleset'            THEN p_data->'ai_ruleset'          ELSE ai_ruleset            END,
            wallet_mode           = CASE WHEN p_data ? 'wallet_mode'           THEN (p_data->>'wallet_mode')::lensers.wallet_mode_enum ELSE wallet_mode END,
            ai_data_usage         = CASE WHEN p_data ? 'ai_data_usage'         THEN (p_data->>'ai_data_usage')::boolean ELSE ai_data_usage  END,
            hide_actions          = CASE WHEN p_data ? 'hide_actions'          THEN (p_data->>'hide_actions')::boolean ELSE hide_actions     END,
            cron_config           = CASE WHEN p_data ? 'cron_config'           THEN p_data->'cron_config'         ELSE cron_config           END,
            selected_api_key_id   = CASE
                                        WHEN p_data ? 'selected_api_key_id' AND p_data->>'selected_api_key_id' IS NULL
                                            THEN NULL
                                        WHEN p_data ? 'selected_api_key_id'
                                            THEN (p_data->>'selected_api_key_id')::uuid
                                        ELSE selected_api_key_id
                                    END,
            updated_at            = now()
        WHERE lenser_id = v_lenser_id
        RETURNING * INTO v_row;
    END IF;

    RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") IS 'Partial-patch update of the authenticated user''s lensers.preferences row. Accepts any subset of preference fields as jsonb — only supplied keys are written. When selected_api_key_id is provided, validates the key is active and owned by this lenser''s human profile OR by the currently active workspace context (e.g. an AI agent the user has switched into). SECURITY DEFINER — runs as postgres, bypasses table-level permission check. Auto-creates the preferences row if missing (legacy profile guard). Added in migration 000061. Updated in 000063 to support selected_api_key_id.';



CREATE OR REPLACE FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_row lensers.profiles;
  v_language text;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.profiles
  SET
    display_name            = CASE WHEN p_data ? 'display_name'            THEN (p_data->>'display_name')                         ELSE display_name            END,
    avatar_url              = CASE WHEN p_data ? 'avatar_url'              THEN (p_data->>'avatar_url')                           ELSE avatar_url              END,
    banner_url              = CASE WHEN p_data ? 'banner_url'              THEN (p_data->>'banner_url')                           ELSE banner_url              END,
    bio                     = CASE WHEN p_data ? 'bio'                     THEN (p_data->>'bio')                                  ELSE bio                     END,
    headline                = CASE WHEN p_data ? 'headline'                THEN (p_data->>'headline')                             ELSE headline                END,
    onboarding_step         = CASE WHEN p_data ? 'onboarding_step'         THEN (p_data->>'onboarding_step')::smallint            ELSE onboarding_step         END,
    onboarding_completed_at = CASE WHEN p_data ? 'onboarding_completed_at' THEN (p_data->>'onboarding_completed_at')::timestamptz ELSE onboarding_completed_at END,
    visibility              = CASE WHEN p_data ? 'visibility'              THEN (p_data->>'visibility')::lensers.lenser_visibility ELSE visibility              END,
    updated_at              = now()
  WHERE id = (
    SELECT id FROM lensers.profiles
    WHERE user_id = v_uid
    ORDER BY created_at ASC
    LIMIT 1
  )
  RETURNING * INTO v_row;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Profile not found';
  END IF;

  IF p_data ? 'preferred_language' THEN
    v_language := COALESCE(NULLIF(btrim(p_data->>'preferred_language'), ''), 'en');
    INSERT INTO lensers.preferences (lenser_id, language)
    VALUES (v_row.id, v_language)
    ON CONFLICT (lenser_id) DO UPDATE
    SET language = EXCLUDED.language,
        updated_at = now();
  END IF;

  RETURN to_jsonb(v_row);
END;
$$;


ALTER FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text" DEFAULT NULL::"text", "p_parent_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "lenses"."versions"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'content', 'public'
    AS $$
  SELECT * FROM lenses.fn_upsert_draft_version(
    p_lens_id, p_template_body, p_changelog, p_parent_version_id
  );
$$;


ALTER FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") RETURNS SETOF "lenses"."versions"
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'lenses', 'public'
    AS $$
  SELECT * FROM lenses.fn_list_versions(p_lens_id);
$$;


ALTER FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
  SELECT lenses.fn_publish_version(p_version_id);
$$;


ALTER FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lenses_starred"() RETURNS SETOF "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lenses', 'lensers'
    AS $$
  SELECT jsonb_build_object(
    'id', l.id,
    'slug', lower(replace(COALESCE(et.title, l.id::text), ' ', '-')),
    'title', COALESCE(et.title, 'Untitled'),
    'author_username', p.handle,
    'starred_at', r.created_at,
    'lifecycle', public.fn_artifact_lifecycle_status('lens', l.id)
  )
  FROM content.reactions r
  JOIN lenses.lenses l ON l.id = r.entity_id
  LEFT JOIN content.entity_translations et
    ON et.entity_id = l.id
   AND et.entity_type = 'lens'::content.entity_type_enum
   AND et.is_original = TRUE
  LEFT JOIN lensers.profiles p ON p.id = l.lenser_id
  WHERE r.entity_type = 'lens'::content.entity_type_enum
    AND r.reaction = 'saved'::content.reaction_enum
    AND r.lenser_id = lensers.get_auth_lenser_id()
    AND public.fn_artifact_can_view('lens', l.id)
  ORDER BY r.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_lenses_starred"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_lifecycle_is_service_role"() RETURNS boolean
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
  SELECT COALESCE(current_setting('request.jwt.claim.role', true), auth.role(), '') = 'service_role';
$$;


ALTER FUNCTION "public"."fn_lifecycle_is_service_role"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "action_type" "text", "context_ref_type" "text", "context_ref_id" "uuid", "result" "text", "metadata" "jsonb", "occurred_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT al.id, al.ai_lenser_id, al.action_type, al.context_ref_type,
         al.context_ref_id, al.result, al.metadata, al.occurred_at
  FROM agents.action_logs al
  WHERE al.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR al.occurred_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = al.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY al.occurred_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ri.*)
  FROM   agents.run_incidents ri
  WHERE  ri.ai_lenser_id = p_ai_lenser_id
    AND  (p_cursor IS NULL OR ri.created_at < p_cursor)
    AND  (
           agents.can_manage_ai_lenser(p_ai_lenser_id)
           OR public.fn_is_super_admin()
         )
  ORDER BY ri.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 20), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Keyset-paginated list of run_incidents for an AI lenser. Returns up to p_limit rows (max 200) ordered by created_at DESC. Caller must own the agent or be a super admin.';



CREATE OR REPLACE FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "is_default" boolean, "category_tags" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT lb.id, lb.ai_lenser_id, lb.lens_id, lb.version_id,
         lb.is_default, lb.category_tags, lb.created_at
  FROM agents.lens_bindings lb
  WHERE lb.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY lb.is_default DESC, lb.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 50, "p_include_redacted" boolean DEFAULT false) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mv.*)
  FROM agents.memories_v mv
  WHERE mv.profile_id = p_profile_id
    AND (p_scope IS NULL OR mv.scope = p_scope)
    AND (p_include_redacted OR mv.is_redacted = false)
    AND (
      EXISTS (
        SELECT 1 FROM agents.memory_profiles mp
        JOIN agents.ownerships o ON o.ai_lenser_id = mp.ai_lenser_id
        WHERE mp.id = p_profile_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY mv.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "model_id" "uuid", "is_default" boolean, "category_tags" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT mb.id, mb.ai_lenser_id, mb.model_id, mb.is_default,
         mb.category_tags, mb.created_at
  FROM agents.model_bindings mb
  WHERE mb.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY mb.is_default DESC, mb.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "owner_lenser_id" "uuid", "role" "text", "permission_scope" "text"[], "granted_at" timestamp with time zone, "revoked_at" timestamp with time zone, "owner_handle" "text", "owner_display_name" "text", "owner_avatar_url" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT
    o.id,
    o.ai_lenser_id,
    o.owner_lenser_id,
    o.role,
    o.permission_scope,
    o.granted_at,
    o.revoked_at,
    p.handle,
    p.display_name,
    p.avatar_url
  FROM agents.ownerships o
  JOIN lensers.profiles p
    ON p.id = o.owner_lenser_id
  WHERE o.ai_lenser_id = p_ai_lenser_id
    AND o.revoked_at IS NULL
    AND agents.can_manage_ai_lenser(p_ai_lenser_id)
  ORDER BY CASE o.role
    WHEN 'owner' THEN 1
    WHEN 'co_owner' THEN 2
    ELSE 3
  END,
  o.granted_at ASC;
$$;


ALTER FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") IS 'Lists active human delegates for an AI Lenser, including the primary owner, co-owners, and scoped operators.';



CREATE OR REPLACE FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ars.*)
  FROM agents.agent_run_steps ars
  JOIN agents.team_runs tr ON tr.id = ars.team_run_id
  WHERE ars.team_run_id = p_team_run_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = tr.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY ars.started_at ASC;
$$;


ALTER FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "name" "text", "description" "text", "status" "text", "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT t.id, t.ai_lenser_id, t.name, t.description, t.status,
         t.is_active, t.created_at, t.updated_at
  FROM agents.teams t
  WHERE t.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR t.created_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY t.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" "uuid" DEFAULT NULL::"uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ti.*)
  FROM   agents.tool_invocations ti
  WHERE  ti.ai_lenser_id = p_ai_lenser_id
    AND  (p_cursor IS NULL OR ti.id < p_cursor)
    AND  (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE  o.ai_lenser_id = p_ai_lenser_id
          AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND  o.revoked_at IS NULL
      )
      OR public.is_admin()
    )
  ORDER  BY ti.created_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(v.*)
  FROM agents.v_agent_profile v
  WHERE v.owner_lenser_id = p_owner_lenser_id
    AND (p_owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY v.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 50) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ar.*)
  FROM agents.approval_requests_v ar
  WHERE ar.ai_lenser_id = p_ai_lenser_id
    AND (p_approval_status IS NULL OR ar.approval_status = p_approval_status)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY ar.requested_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_automation_rules"("p_limit" integer DEFAULT 100, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "name" "text", "match_event_type" "text", "match_filter" "jsonb", "action_kind" "text", "action_config" "jsonb", "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  SELECT
    tr.id,
    tr.lenser_id,
    tr.name,
    tr.match_event_type,
    tr.match_filter,
    tr.action_kind,
    tr.action_config,
    tr.is_active,
    tr.created_at,
    tr.updated_at
  FROM automation.trigger_rules tr
  WHERE tr.lenser_id = auth.uid()
    AND (p_cursor IS NULL OR tr.created_at < p_cursor)
  ORDER BY tr.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: list automation trigger rules for the current user. Keyset-paginated by created_at DESC. Max 200 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_list_battle_templates"("p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "title" "text", "description" "text", "task_prompt" "text", "is_public" boolean, "max_contenders" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT t.id, t.title, t.description, t.task_prompt,
         t.is_public, t.max_contenders, t.created_at, t.updated_at
  FROM battles.templates t
  WHERE t.deleted_at IS NULL
    AND (p_cursor IS NULL OR t.created_at < p_cursor)
  ORDER BY t.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ec.*)
  FROM agents.evaluation_cases ec
  JOIN agents.evaluations ev ON ev.id = ec.evaluation_id
  WHERE ec.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY ec.created_at ASC;
$$;


ALTER FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(erub.*)
  FROM agents.evaluation_rubrics erub
  JOIN agents.evaluations ev ON ev.id = erub.evaluation_id
  WHERE erub.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY erub.version DESC;
$$;


ALTER FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(er.*)
  FROM agents.evaluation_runs er
  JOIN agents.evaluations ev ON ev.id = er.evaluation_id
  WHERE er.evaluation_id = p_evaluation_id
    AND (ev.owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY er.started_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ev.*)
  FROM agents.evaluations ev
  WHERE ev.owner_lenser_id = p_owner_lenser_id
    AND (p_cursor IS NULL OR ev.created_at < p_cursor)
    AND (p_owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY ev.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer DEFAULT 50) RETURNS TABLE("device_id" "uuid", "hostname" "text", "daemon_version" "text", "last_seen_at" timestamp with time zone, "approved_at" timestamp with time zone, "revoked_at" timestamp with time zone, "kill_switch" boolean, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'extensions'
    AS $$
  SELECT * FROM agents.fn_list_gateway_devices(p_limit);
$$;


ALTER FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean DEFAULT false) RETURNS TABLE("id" "uuid", "lens_id" "uuid", "version_number" integer, "status" "text", "changelog" "text", "parameter_count" integer, "published_at" timestamp with time zone, "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    v.id, v.lens_id, v.version_number, v.status::text,
    v.changelog, v.parameter_count, v.published_at, v.created_at
  FROM lenses.vw_lens_version_history v
  JOIN lenses.lenses l ON l.id = v.lens_id
  WHERE v.lens_id = p_lens_id
    AND (
      l.lenser_id = lensers.get_auth_lenser_id()
      OR (l.visibility = 'public' AND v.status = 'published')
    )
    AND (p_include_archived OR v.status <> 'archived')
  ORDER BY v.version_number DESC;
$$;


ALTER FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) IS 'Security wrapper: list versions for a lens. Owner sees all (optionally archived); others see published versions of public lenses only.';



CREATE OR REPLACE FUNCTION "public"."fn_list_media_objects"("p_limit" integer DEFAULT 200, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "owner_lenser_id" "uuid", "bucket" "text", "object_key" "text", "external_url" "text", "mime_type" "text", "media_type" "text", "name" "text", "content_text" "text", "byte_size" bigint, "visibility" "text", "lifecycle_state" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'media', 'lensers'
    AS $$
  SELECT
    o.id, o.workspace_id, o.owner_lenser_id, o.bucket, o.object_key,
    o.external_url, o.mime_type, o.media_type, o.name, o.content_text,
    o.byte_size, o.visibility, o.lifecycle_state, o.metadata,
    o.created_at, o.updated_at
  FROM media.objects o
  WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
    AND o.lifecycle_state <> 'deleted'
    AND (p_cursor IS NULL OR o.created_at < p_cursor)
  ORDER BY o.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 200), 1), 500);
$$;


ALTER FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: list non-deleted media objects owned by the current user. Keyset-paginated by created_at DESC. Max 500 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer DEFAULT 50) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mal.*)
  FROM agents.memory_access_logs mal
  WHERE mal.memory_id = p_memory_id
  ORDER BY mal.accessed_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mp.*)
  FROM agents.memory_profiles mp
  WHERE mp.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY mp.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(mop.*)
  FROM agents.model_profiles mop
  WHERE mop.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY mop.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "visibility" "text", "parent_lens_id" "uuid", "forked_from_execution_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    l.id, l.lenser_id, l.visibility, l.parent_lens_id,
    l.forked_from_execution_id, l.created_at, l.updated_at
  FROM lenses.lenses l
  WHERE l.lenser_id = lensers.get_auth_lenser_id()
    AND l.visibility <> 'public'
    AND (p_cursor IS NULL OR l.created_at < p_cursor)
  ORDER BY l.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) IS 'Security wrapper: list the current user''s non-public lenses (private/draft). Keyset-paginated by created_at DESC. Max 200 rows per call.';



CREATE OR REPLACE FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(pp.*)
  FROM agents.personality_profiles pp
  WHERE pp.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY pp.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 100, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(pe.*)
  FROM   agents.policy_evaluations pe
  WHERE  pe.ai_lenser_id = p_ai_lenser_id
    AND  (p_cursor IS NULL OR pe.evaluated_at < p_cursor)
    AND  (
           agents.can_manage_ai_lenser(p_ai_lenser_id)
           OR public.fn_is_super_admin()
         )
  ORDER BY pe.evaluated_at DESC
  LIMIT  LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
$$;


ALTER FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) IS 'Keyset-paginated list of policy_evaluations for an AI lenser ordered by evaluated_at DESC. Returns up to p_limit rows (max 500). Caller must own the agent or be a super admin.';



CREATE OR REPLACE FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "title" "text", "description" "text", "task_prompt" "text", "category" "text", "is_public" boolean, "max_contenders" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT t.id, t.title, t.description,
         -- task_prompt only returned to authenticated callers; anon gets NULL to
         -- prevent accidental full-prompt exposure of mistakenly-public templates.
         CASE WHEN auth.uid() IS NOT NULL THEN t.task_prompt ELSE NULL END AS task_prompt,
         t.category, t.is_public, t.max_contenders, t.created_at, t.updated_at
    FROM battles.templates t
   WHERE t.is_public = true
     AND t.deleted_at IS NULL
     AND (p_category IS NULL OR t.category = p_category)
   ORDER BY t.created_at DESC
   LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(al.*)
  FROM agents.action_logs al
  WHERE al.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR al.occurred_at < p_cursor)
    AND al.result = 'error'
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY al.occurred_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text" DEFAULT NULL::"text", "p_resolved" boolean DEFAULT NULL::boolean, "p_limit" integer DEFAULT 100) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ri.*)
  FROM agents.run_incidents ri
  JOIN agents.run_reports rr ON rr.id = ri.run_report_id
  WHERE ri.run_report_id = p_run_report_id
    AND (p_severity IS NULL OR ri.severity::text = p_severity)
    AND (p_resolved IS NULL
         OR (p_resolved = true AND ri.resolved_at IS NOT NULL)
         OR (p_resolved = false AND ri.resolved_at IS NULL))
    AND (
      rr.ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
          AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY ri.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 500);
$$;


ALTER FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT to_jsonb(rr.*)
  FROM agents.run_reports rr
  WHERE rr.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR rr.created_at < p_cursor)
  ORDER BY rr.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 20, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(sr.*)
  FROM agents.scratchpad_runs sr
  WHERE sr.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR sr.started_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY sr.started_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100);
$$;


ALTER FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(te.*)
  FROM agents.team_edges te
  JOIN agents.teams t ON t.id = te.team_id
  WHERE te.team_id = p_team_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = t.ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY te.created_at;
$$;


ALTER FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_template_workflows"("p_limit" integer DEFAULT 12, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "node_count" bigint, "reaction_totals" "jsonb", "fork_count" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "author_handle" "text", "author_display_name" "text", "kinds" "text"[])
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers'
    AS $$
  WITH tmpl AS (
    SELECT tm.entity_id AS workflow_id, tm.created_at AS marked_at
    FROM content.tag_map tm
    JOIN content.tags t ON t.id = tm.tag_id
    WHERE tm.entity_type = 'workflow'::content.entity_type_enum
      AND t.slug = 'template'
  )
  SELECT
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility::text,
    (
      SELECT count(*) FROM lenses.workflow_nodes wn WHERE wn.workflow_id = w.id
    ) AS node_count,
    w.reaction_totals,
    w.fork_count,
    w.created_at,
    w.updated_at,
    p.handle             AS author_handle,
    p.display_name       AS author_display_name,
    COALESCE(
      (
        SELECT array_agg(DISTINCT t.slug ORDER BY t.slug)
        FROM   lenses.workflow_nodes wn
        JOIN   lenses.lenses   ll  ON ll.id  = wn.lens_id
        JOIN   content.tag_map tm2 ON tm2.entity_id   = ll.id
                                   AND tm2.entity_type = 'lens'::content.entity_type_enum
        JOIN   content.tags    t   ON t.id = tm2.tag_id
                                   AND t.slug IN (
                                     'text','image','video','audio','music',
                                     'research','pdf','transform','orchestration',
                                     'validation','routing',
                                     'code','data','planning','community','documentation'
                                   )
        WHERE  wn.workflow_id = w.id
      ),
      ARRAY[]::text[]
    ) AS kinds
  FROM  tmpl
  JOIN  lenses.workflows   w ON w.id  = tmpl.workflow_id
  LEFT JOIN lensers.profiles p ON p.id = w.lenser_id
  WHERE w.visibility::text = 'public'
  ORDER BY tmpl.marked_at ASC, w.title ASC
  LIMIT  p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) IS 'Lists curated template workflows that power the "Start from template" strip on WorkflowsPage. A workflow is considered a template when it has a content.tag_map row linking it to the `template` tag.';



CREATE OR REPLACE FUNCTION "public"."fn_list_threads"("p_cursor" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20, "p_tag_slug" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "author_lenser_id" "uuid", "lenser_id" "uuid", "visibility" "text", "title" "text", "content" "text", "lenser_handle" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "reply_count" integer, "view_count" integer, "thumbnail_url" "text", "linked_lens_id" "uuid", "lens_data" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  SELECT
    t.id,
    t.lenser_id  AS author_lenser_id,
    t.lenser_id,
    t.visibility::text,
    COALESCE(et.title, 'Untitled') AS title,
    COALESCE(et.content, '')       AS content,
    prof.handle                    AS lenser_handle,
    t.created_at,
    t.updated_at,
    t.reply_count,
    t.view_count,
    t.thumbnail_url,
    t.linked_lens_id,
    t.lens_data
  FROM content.threads t
  LEFT JOIN content.entity_translations et
         ON et.entity_id   = t.id
        AND et.entity_type = 'thread'::content.entity_type_enum
        AND et.is_original = true
  LEFT JOIN lensers.profiles prof ON prof.id = t.lenser_id
  WHERE t.lenser_id = lensers.get_auth_lenser_id()
    AND (
      p_cursor IS NULL
      OR t.created_at < (SELECT ct.created_at FROM content.threads ct WHERE ct.id = p_cursor)
    )
    AND (
      p_tag_slug IS NULL
      OR EXISTS (
        SELECT 1
        FROM content.tag_map tm
        JOIN content.tags tg ON tg.id = tm.tag_id
        WHERE tm.entity_id   = t.id
          AND tm.entity_type = 'thread'::content.entity_type_enum
          AND tg.slug = p_tag_slug
      )
    )
  ORDER BY t.created_at DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") IS 'Lists threads owned by the current authenticated user (all visibilities) with title, content, and lenser_handle pre-joined. Used by threadsRepository.getByLenser(includePrivate=true) when the viewer is the profile owner. Supports cursor pagination and optional tag-slug filtering. Title/content come from entity_translations so private threads hydrate correctly without a second public-view lookup.';



CREATE OR REPLACE FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(ta.*)
  FROM agents.tool_assignments ta
  WHERE ta.ai_lenser_id = p_ai_lenser_id
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    );
$$;


ALTER FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(tp.*)
  FROM agents.tool_profiles tp
  WHERE tp.ai_lenser_id = p_ai_lenser_id
    AND (
      p_ai_lenser_id IN (
        SELECT o.ai_lenser_id FROM agents.ownerships o
        WHERE o.owner_lenser_id = lensers.get_auth_lenser_id() AND o.revoked_at IS NULL
      )
      OR public.fn_is_super_admin()
    )
  ORDER BY tp.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tools"("p_category" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT lenses.fn_list_tools(p_category);
$$;


ALTER FUNCTION "public"."fn_list_tools"("p_category" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(tr.*)
  FROM agents.tools_registry tr
  WHERE tr.owner_lenser_id = p_owner_lenser_id
    AND (p_owner_lenser_id = lensers.get_auth_human_lenser_id() OR public.is_admin())
  ORDER BY tr.name ASC;
$$;


ALTER FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_tournaments"("p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT to_jsonb(t.*)
  FROM battles.tournaments t
  WHERE (p_cursor IS NULL OR t.created_at < p_cursor)
  ORDER BY t.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer DEFAULT 50, "p_cursor" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  SELECT to_jsonb(wa.*)
  FROM agents.workflow_assignments wa
  WHERE wa.ai_lenser_id = p_ai_lenser_id
    AND (p_cursor IS NULL OR wa.created_at < p_cursor)
    AND (
      EXISTS (
        SELECT 1 FROM agents.ownerships o
        WHERE o.ai_lenser_id = p_ai_lenser_id
          AND o.owner_lenser_id = lensers.get_auth_human_lenser_id()
          AND o.revoked_at IS NULL
      ) OR public.is_admin()
    )
  ORDER BY wa.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 50), 1), 200);
$$;


ALTER FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT to_jsonb(wp.*)
  FROM lenses.workflow_phases wp
  JOIN lenses.workflows w ON w.id = wp.workflow_id
  WHERE wp.workflow_id = p_workflow_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wp.ordinal, wp.created_at;
$$;


ALTER FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint DEFAULT 0, "p_limit" integer DEFAULT 200) RETURNS TABLE("event_id" bigint, "type" "text", "run_id" "uuid", "occurred_at" timestamp with time zone, "payload" "jsonb")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    e.event_id,
    e.type,
    e.run_id,
    e.created_at AS occurred_at,
    e.payload
  FROM lenses.workflow_run_events e
  JOIN lenses.workflow_runs r ON r.id = e.run_id
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE e.run_id = p_run_id
    AND e.event_id > COALESCE(p_after_event_id, 0)
    AND (
      r.triggered_by = lensers.get_auth_lenser_id()
      OR w.visibility::text = 'public'
    )
  ORDER BY e.event_id ASC
  LIMIT GREATEST(1, LEAST(COALESCE(p_limit, 200), 1000));
$$;


ALTER FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) IS 'Lists workflow run events after p_after_event_id for SSE replay/catch-up.';



CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer DEFAULT 20, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "status" "text", "trigger_mode" "text", "started_at" timestamp with time zone, "completed_at" timestamp with time zone, "created_at" timestamp with time zone, "global_model_id" "text", "spent_credits" integer)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT
    r.id,
    r.workflow_id,
    r.status,
    r.trigger_mode,
    r.started_at,
    r.completed_at,
    r.created_at,
    r.global_model_id,
    r.spent_credits
  FROM lenses.workflow_runs r
  JOIN lenses.workflows w ON w.id = r.workflow_id
  WHERE r.workflow_id = p_workflow_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY r.created_at DESC
  LIMIT LEAST(GREATEST(COALESCE(p_limit, 20), 1), 100)
  OFFSET GREATEST(COALESCE(p_offset, 0), 0);
$$;


ALTER FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) IS 'Owner-only paginated list of workflow runs for a given workflow. Used by the workflow builder history tab.';



CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT to_jsonb(wt.*)
  FROM lenses.workflow_tasks wt
  JOIN lenses.workflows w ON w.id = wt.workflow_id
  WHERE wt.phase_id = p_phase_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wt.ordinal, wt.created_at;
$$;


ALTER FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") RETURNS SETOF "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  SELECT to_jsonb(wt.*)
  FROM lenses.workflow_tasks wt
  JOIN lenses.workflows w ON w.id = wt.workflow_id
  WHERE wt.workflow_id = p_workflow_id
    AND w.lenser_id = lensers.get_auth_lenser_id()
  ORDER BY wt.ordinal, wt.created_at;
$$;


ALTER FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'audit', 'battles'
    AS $$
BEGIN
  INSERT INTO audit.events (
    event_type,
    entity_schema,
    entity_table,
    entity_id,
    actor_type,
    actor_id,
    actor_lenser_id,
    payload,
    severity
  ) VALUES (
    'artifact.' || p_action,
    CASE p_artifact_type
      WHEN 'lens' THEN 'lenses'
      WHEN 'workflow' THEN 'lenses'
      WHEN 'battle' THEN 'battles'
      WHEN 'agent' THEN 'agents'
      ELSE 'public'
    END,
    CASE p_artifact_type
      WHEN 'lens' THEN 'lenses'
      WHEN 'workflow' THEN 'workflows'
      WHEN 'battle' THEN 'battles'
      WHEN 'agent' THEN 'ai_lensers'
      ELSE p_artifact_type
    END,
    p_artifact_id,
    CASE WHEN p_actor_id IS NULL THEN 'system' ELSE 'lenser' END,
    p_actor_id,
    p_actor_id,
    COALESCE(p_payload, '{}'::jsonb),
    'info'
  );
EXCEPTION WHEN undefined_table THEN
  NULL;
END;
$$;


ALTER FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb" DEFAULT NULL::"jsonb", "p_violations" "jsonb" DEFAULT '[]'::"jsonb") RETURNS "battles"."model_test_runs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'extensions'
    AS $$
DECLARE
  v_uid UUID := auth.uid();
  v_row battles.model_test_runs%ROWTYPE;
BEGIN
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;
  IF p_battle_id IS NULL AND p_template_id IS NULL THEN
    RAISE EXCEPTION 'battle_or_template_required' USING ERRCODE = '22023';
  END IF;

  -- Caller must own the anchor row.
  IF p_battle_id IS NOT NULL
     AND NOT EXISTS (
       SELECT 1 FROM battles.battles
        WHERE id = p_battle_id AND creator_lenser_id = v_uid
     ) THEN
    RAISE EXCEPTION 'battle_not_owned' USING ERRCODE = '42501';
  END IF;
  IF p_template_id IS NOT NULL
     AND NOT EXISTS (
       SELECT 1 FROM battles.templates
        WHERE id = p_template_id AND creator_lenser_id = v_uid
     ) THEN
    RAISE EXCEPTION 'template_not_owned' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.model_test_runs (
    battle_id, template_id, model_provider, model_id,
    prompt_hash, passed, duration_ms, raw_output, violations
  ) VALUES (
    p_battle_id, p_template_id, p_model_provider, p_model_id,
    p_prompt_hash, p_passed, p_duration_ms, p_raw_output,
    COALESCE(p_violations, '[]'::jsonb)
  )
  RETURNING * INTO v_row;

  RETURN v_row;
END $$;


ALTER FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'auth', 'analytics', 'lensers'
    AS $$
DECLARE
  v_user_id   uuid;
  v_lenser_id uuid;
BEGIN
  -- 1. Validation Logic
  IF p_target_type IS NULL THEN
    RAISE EXCEPTION 'target_type is required';
  END IF;

  IF p_path IS NULL THEN
    RAISE EXCEPTION 'path is required';
  END IF;

  -- 2. Identity Resolution
  v_user_id := auth.uid();

  -- Resilience: Verify user exists in auth.users to prevent FK violations from stale JWTs
  -- If user does not exist (e.g., deleted or stale session), fallback to anonymous logging
  IF v_user_id IS NOT NULL THEN
    IF NOT EXISTS (SELECT 1 FROM auth.users WHERE id = v_user_id) THEN
      v_user_id := NULL;
    END IF;
  END IF;

  -- Resolve lenser profile if user is valid
  IF v_user_id IS NOT NULL THEN
    SELECT id
    INTO v_lenser_id
    FROM lensers.profiles
    WHERE user_id = v_user_id;
  END IF;

  -- 3. Debouncing (Avoid duplicate logs within 5 seconds for same user/path)
  IF v_user_id IS NOT NULL THEN
    IF EXISTS (
      SELECT 1
      FROM analytics.page_views
      WHERE user_id = v_user_id
        AND path = p_path
        AND created_at > NOW() - INTERVAL '5 seconds'
    ) THEN
      RETURN;
    END IF;
  END IF;

  -- 4. Audit Log Execution
  INSERT INTO analytics.page_views (
    lenser_id,
    user_id,
    target_type,
    target_id,
    path,
    referrer,
    user_agent,
    client_ip
  )
  VALUES (
    v_lenser_id,
    v_user_id,
    p_target_type,
    p_target_id,
    p_path,
    p_referrer,
    p_user_agent,
    p_client_ip
  );
END;
$$;


ALTER FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_updated INT;
BEGIN
  UPDATE public.notifications
  SET    read_at = now()
  WHERE  id = ANY(p_notification_ids)
  AND    lenser_id = lensers.get_auth_lenser_id()
  AND    read_at IS NULL;

  GET DIAGNOSTICS v_updated = ROW_COUNT;
  RETURN v_updated;
END;
$$;


ALTER FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text" DEFAULT 'tutorial'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'xp', 'public', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_row_id    UUID;
  v_inserted  BOOLEAN := false;
BEGIN
  IF p_kind NOT IN ('tutorial', 'walkthrough') THEN
    RAISE EXCEPTION 'invalid_kind'
      USING ERRCODE = '22023',
            DETAIL  = 'kind must be ''tutorial'' or ''walkthrough''',
            HINT    = 'invalid_kind';
  END IF;

  v_lenser_id := lensers.get_auth_lenser_id();
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'Must be authenticated.',
            HINT    = 'permission_denied';
  END IF;

  INSERT INTO lenses.tutorial_completions (lenser_id, tutorial_slug, kind)
  VALUES (v_lenser_id, p_tutorial_slug, p_kind)
  ON CONFLICT (lenser_id, tutorial_slug, kind) DO NOTHING
  RETURNING id INTO v_row_id;

  v_inserted := v_row_id IS NOT NULL;

  RETURN jsonb_build_object(
    'inserted',       v_inserted,
    'tutorial_slug',  p_tutorial_slug,
    'kind',           p_kind
  );
END;
$$;


ALTER FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") IS 'CB-P1b: Marks a tutorial or walkthrough as complete for the calling lenser. Idempotent. Fires XP award trigger on first completion. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text" DEFAULT '_default'::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
DECLARE
    v_owner uuid;
    v_attachment_id uuid;
BEGIN
    SELECT owner_lenser_id INTO v_owner
    FROM media.objects WHERE id = p_object_id;

    IF v_owner IS NULL THEN
        RAISE EXCEPTION 'Media object not found: %', p_object_id;
    END IF;

    IF v_owner <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Permission denied: not object owner';
    END IF;

    INSERT INTO media.attachments (object_id, entity_type, entity_id, binding_key, attached_by)
    VALUES (p_object_id, p_entity_type, p_entity_id, p_binding_key, lensers.get_auth_lenser_id())
    ON CONFLICT (entity_type, entity_id, binding_key)
    DO UPDATE SET object_id = EXCLUDED.object_id, attached_by = EXCLUDED.attached_by, attached_at = now()
    RETURNING id INTO v_attachment_id;

    RETURN v_attachment_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") IS 'Binds a media object to a business entity via a named slot. Upserts: if the (entity_type, entity_id, binding_key) already exists, replaces the bound object.';



CREATE OR REPLACE FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint DEFAULT NULL::bigint, "p_checksum" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
DECLARE
    v_owner uuid;
BEGIN
    SELECT owner_lenser_id INTO v_owner
    FROM media.objects WHERE id = p_object_id;

    IF v_owner IS NULL THEN
        RAISE EXCEPTION 'Media object not found: %', p_object_id;
    END IF;

    IF v_owner <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Permission denied: not object owner';
    END IF;

    UPDATE media.objects
    SET bucket = p_bucket,
        object_key = p_object_key,
        byte_size = COALESCE(p_byte_size, byte_size),
        checksum_sha256 = COALESCE(p_checksum, checksum_sha256),
        lifecycle_state = 'active',
        updated_at = now()
    WHERE id = p_object_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") IS 'Finalizes a media object upload. Sets bucket, object_key, byte_size, checksum, and transitions lifecycle_state to active. Validates ownership.';



CREATE OR REPLACE FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'public'
    AS $$
BEGIN
  UPDATE media.objects
  SET access_count = access_count + 1
  WHERE id = p_object_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") IS 'AT: Increments media.objects.access_count. Called by media-proxy route after successful signed URL generation. Fire-and-forget; errors do not affect the proxy response. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
DECLARE
    v_owner uuid;
BEGIN
    SELECT owner_lenser_id INTO v_owner
    FROM media.objects WHERE id = p_object_id;

    IF v_owner IS NULL THEN
        RAISE EXCEPTION 'Media object not found: %', p_object_id;
    END IF;

    IF v_owner <> lensers.get_auth_lenser_id() THEN
        RAISE EXCEPTION 'Permission denied: not object owner';
    END IF;

    UPDATE media.objects
    SET lifecycle_state = 'deleted', updated_at = now()
    WHERE id = p_object_id;
END;
$$;


ALTER FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text" DEFAULT '_default'::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'lensers', 'public'
    AS $$
BEGIN
    DELETE FROM media.attachments
    WHERE entity_type = p_entity_type
      AND entity_id = p_entity_id
      AND binding_key = p_binding_key
      AND EXISTS (
          SELECT 1 FROM media.objects o
          WHERE o.id = attachments.object_id
            AND o.owner_lenser_id = lensers.get_auth_lenser_id()
      );
END;
$$;


ALTER FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text" DEFAULT NULL::"text", "p_error_msg" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
DECLARE
  v_dlq_id uuid;
BEGIN
  SELECT battles.fn_move_battle_job_to_dlq(p_job_id, p_error_code, p_error_msg)
  INTO v_dlq_id;
  RETURN v_dlq_id;
END;
$$;


ALTER FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_battle        battles.battles;
  v_winner        battles.contenders;
  v_action_url    TEXT;
  v_email_payload JSONB;
  v_voter_ids     UUID[];
  v_contender     RECORD;
  v_outcome_type  TEXT;
  v_body_text     TEXT;
BEGIN
  SELECT * INTO v_battle FROM battles.battles WHERE id = p_battle_id;
  IF NOT FOUND THEN RETURN; END IF;

  IF v_battle.winner_contender_id IS NOT NULL THEN
    SELECT * INTO v_winner FROM battles.contenders WHERE id = v_battle.winner_contender_id;
    v_body_text := 'Winner: ' || COALESCE(v_winner.display_name, 'Unknown');
  ELSE
    v_body_text := 'The battle ended in a draw.';
  END IF;

  v_action_url := '/battles/' || v_battle.slug || '/result';

  -- ── Contenders: differentiated battle_won / battle_lost ──────────────────
  FOR v_contender IN
    SELECT
      cem.profile_id                                                AS lenser_id,
      c.id                                                          AS contender_id,
      (c.id = v_battle.winner_contender_id)                        AS is_winner
    FROM   battles.contenders c
    JOIN   battles.contender_entity_map cem ON cem.contender_id = c.id
    WHERE  c.battle_id     = p_battle_id
      AND  cem.profile_id  IS NOT NULL
  LOOP
    v_outcome_type := CASE WHEN v_contender.is_winner THEN 'battle_won' ELSE 'battle_lost' END;

    PERFORM public.fn_insert_notification(
      v_contender.lenser_id,
      v_outcome_type,
      'Battle result: ' || v_battle.title,
      v_body_text,
      v_action_url,
      jsonb_build_object(
        'battle_id',     p_battle_id,
        'battle_slug',   v_battle.slug,
        'contender_id',  v_contender.contender_id,
        'winner_id',     v_battle.winner_contender_id,
        'winner_name',   COALESCE(v_winner.display_name, NULL),
        'is_winner',     v_contender.is_winner
      )
      -- p_actor_id = NULL: system outcome, skip block/mute gate
    );
  END LOOP;

  -- ── Voters: keep generic battle_result ───────────────────────────────────
  SELECT array_agg(DISTINCT voter_lenser_id) INTO v_voter_ids
  FROM   battles.votes
  WHERE  battle_id         = p_battle_id
    AND  voter_lenser_id   IS NOT NULL;

  IF v_voter_ids IS NOT NULL AND array_length(v_voter_ids, 1) > 0 THEN
    INSERT INTO public.notifications (lenser_id, type, title, body, action_url, metadata)
    SELECT
      unnest(v_voter_ids),
      'battle_result',
      'Battle result: ' || v_battle.title,
      v_body_text,
      v_action_url,
      jsonb_build_object(
        'battle_id',   p_battle_id,
        'battle_slug', v_battle.slug,
        'winner_id',   v_battle.winner_contender_id,
        'winner_name', COALESCE(v_winner.display_name, NULL)
      )
    ON CONFLICT DO NOTHING;
  END IF;

  -- ── Email edge function (unchanged) ───────────────────────────────────────
  IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net') THEN
    SELECT jsonb_build_object(
      'battle_id',        p_battle_id,
      'battle_title',     v_battle.title,
      'battle_slug',      v_battle.slug,
      'winner_name',      COALESCE(v_winner.display_name, NULL),
      'recipient_emails', (
        SELECT jsonb_agg(email)
        FROM   lensers.profiles
        WHERE  id = ANY(v_voter_ids)
          AND  email IS NOT NULL
      )
    ) INTO v_email_payload;

    PERFORM net.http_post(
      url     := current_setting('app.supabase_url', true) || '/functions/v1/send-battle-result-email',
      headers := jsonb_build_object(
        'Content-Type',  'application/json',
        'Authorization', 'Bearer ' || internal.get_service_role_key()
      ),
      body    := v_email_payload
    );
  END IF;

EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_notify_battle_result failed for battle %: %', p_battle_id, SQLERRM;
END;
$$;


ALTER FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'vault'
    AS $$
DECLARE
  v_conn          public.user_oauth_connections%ROWTYPE;
  v_refresh_token text;
BEGIN
  SELECT * INTO v_conn
  FROM public.user_oauth_connections
  WHERE lenser_id = p_lenser_id
    AND ref       = p_ref
    AND (p_workspace_id IS NULL OR workspace_id = p_workspace_id)
    AND is_active = true
    AND revoked_at IS NULL;

  IF NOT FOUND THEN
    RETURN NULL;
  END IF;

  -- No refresh token stored — caller cannot refresh
  IF v_conn.refresh_token_id IS NULL THEN
    RETURN NULL;
  END IF;

  -- Not near expiry (> 5 min remaining) — no refresh needed
  IF v_conn.expires_at IS NOT NULL
     AND v_conn.expires_at > (now() + interval '5 minutes')
  THEN
    RETURN NULL;
  END IF;

  SELECT decrypted_secret INTO v_refresh_token
  FROM vault.decrypted_secrets
  WHERE id = v_conn.refresh_token_id;

  IF v_refresh_token IS NULL THEN
    RETURN NULL;
  END IF;

  RETURN jsonb_build_object(
    'connection_id',   v_conn.id,
    'workspace_id',    v_conn.workspace_id,
    'refresh_token',   v_refresh_token,
    'expires_at',      v_conn.expires_at,
    'granted_scopes',  v_conn.granted_scopes,
    'provider',        v_conn.provider,
    'capability',      v_conn.capability
  );
END;
$$;


ALTER FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_list_connections"() RETURNS TABLE("id" "uuid", "workspace_id" "uuid", "provider" "text", "capability" "text", "connection_label" "text", "ref" "text", "granted_scopes" "text"[], "expires_at" timestamp with time zone, "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
  SELECT
    c.id,
    c.workspace_id,
    c.provider,
    c.capability,
    c.connection_label,
    c.ref,
    c.granted_scopes,
    c.expires_at,
    c.is_active,
    c.created_at,
    c.updated_at
  FROM public.user_oauth_connections c
  JOIN lensers.profiles p ON p.id = c.lenser_id
  WHERE p.user_id = auth.uid()
    AND c.revoked_at IS NULL
  ORDER BY c.provider, c.capability, c.created_at DESC;
$$;


ALTER FUNCTION "public"."fn_oauth_list_connections"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[] DEFAULT '{}'::"text"[], "p_workspace_id" "uuid" DEFAULT NULL::"uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'vault'
    AS $$
DECLARE
  v_conn         public.user_oauth_connections%ROWTYPE;
  v_access_token text;
BEGIN
  SELECT * INTO v_conn
  FROM public.user_oauth_connections
  WHERE lenser_id = p_lenser_id
    AND ref       = p_ref
    AND (p_workspace_id IS NULL OR workspace_id = p_workspace_id)
    AND is_active = true
    AND revoked_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'oauth_resolve_not_found: ref=% lenser=%', p_ref, p_lenser_id
      USING ERRCODE = 'P0002';
  END IF;

  -- Scope containment check: required ⊆ granted
  IF array_length(p_required_scopes, 1) > 0 THEN
    IF NOT (p_required_scopes <@ v_conn.granted_scopes) THEN
      RAISE EXCEPTION
        'oauth_resolve_scope_mismatch: ref=%, required=%, granted=%',
        p_ref, p_required_scopes, v_conn.granted_scopes
        USING ERRCODE = '42501';
    END IF;
  END IF;

  -- Retrieve access token from vault
  SELECT decrypted_secret INTO v_access_token
  FROM vault.decrypted_secrets
  WHERE id = v_conn.access_token_id;

  IF v_access_token IS NULL THEN
    RAISE EXCEPTION 'oauth_resolve_vault_miss: vault secret missing for ref=%', p_ref
      USING ERRCODE = 'P0001';
  END IF;

  RETURN v_access_token;
END;
$$;


ALTER FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid();

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'oauth_revoke_unauthorized: no profile for caller'
      USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM public.user_oauth_connections
    WHERE id = p_connection_id
      AND lenser_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'oauth_revoke_forbidden: connection not found or not owned'
      USING ERRCODE = '42501';
  END IF;

  UPDATE public.user_oauth_connections
  SET
    is_active  = false,
    revoked_at = now(),
    updated_at = now()
  WHERE id = p_connection_id
    AND lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text" DEFAULT NULL::"text", "p_granted_scopes" "text"[] DEFAULT '{}'::"text"[], "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'vault'
    AS $$
DECLARE
  v_ref            text;
  v_access_name    text;
  v_refresh_name   text;
  v_access_id      uuid;
  v_refresh_id     uuid;
  v_conn_id        uuid;
BEGIN
  v_ref          := p_provider || '.' || p_capability || '.' || p_label;
  v_access_name  := 'oauth_access_'  || p_lenser_id::text || '_' || v_ref;
  v_refresh_name := 'oauth_refresh_' || p_lenser_id::text || '_' || v_ref;

  -- Store or rotate access token in vault
  SELECT id INTO v_access_id
  FROM vault.secrets
  WHERE name = v_access_name
  LIMIT 1;

  IF v_access_id IS NULL THEN
    INSERT INTO vault.secrets (name, secret, description)
    VALUES (v_access_name, p_access_token, 'OAuth access token: ' || v_ref)
    RETURNING id INTO v_access_id;
  ELSE
    UPDATE vault.secrets
    SET secret = p_access_token
    WHERE id = v_access_id;
  END IF;

  -- Store or rotate refresh token in vault (if provided)
  IF p_refresh_token IS NOT NULL THEN
    SELECT id INTO v_refresh_id
    FROM vault.secrets
    WHERE name = v_refresh_name
    LIMIT 1;

    IF v_refresh_id IS NULL THEN
      INSERT INTO vault.secrets (name, secret, description)
      VALUES (v_refresh_name, p_refresh_token, 'OAuth refresh token: ' || v_ref)
      RETURNING id INTO v_refresh_id;
    ELSE
      UPDATE vault.secrets
      SET secret = p_refresh_token
      WHERE id = v_refresh_id;
    END IF;
  END IF;

  -- Upsert the connection row (on conflict: update tokens + scopes + expiry)
  INSERT INTO public.user_oauth_connections (
    lenser_id,
    workspace_id,
    provider,
    capability,
    connection_label,
    ref,
    access_token_id,
    refresh_token_id,
    granted_scopes,
    expires_at,
    is_active,
    revoked_at,
    updated_at
  )
  VALUES (
    p_lenser_id,
    p_workspace_id,
    p_provider,
    p_capability,
    p_label,
    v_ref,
    v_access_id,
    v_refresh_id,
    p_granted_scopes,
    p_expires_at,
    true,
    NULL,
    now()
  )
  ON CONFLICT (lenser_id, workspace_id, ref) DO UPDATE
    SET access_token_id  = EXCLUDED.access_token_id,
        -- Only update refresh_token_id if a new one was provided
        refresh_token_id = COALESCE(
          CASE WHEN p_refresh_token IS NOT NULL THEN EXCLUDED.refresh_token_id END,
          user_oauth_connections.refresh_token_id
        ),
        granted_scopes   = EXCLUDED.granted_scopes,
        expires_at       = EXCLUDED.expires_at,
        is_active        = true,
        revoked_at       = NULL,
        updated_at       = now()
  RETURNING id INTO v_conn_id;

  RETURN v_conn_id;
END;
$$;


ALTER FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean DEFAULT false, "p_ip_address" "text" DEFAULT NULL::"text", "p_user_agent" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'analytics', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  -- Basic sanity guards
  IF trim(p_name) = '' OR trim(p_email) = '' OR trim(p_message) = '' THEN
    RAISE EXCEPTION 'name, email, and message are required' USING ERRCODE = '22023';
  END IF;

  IF NOT p_kvkk_approved THEN
    RAISE EXCEPTION 'KVKK consent is required' USING ERRCODE = '22023';
  END IF;

  -- Resolve optional lenser_id from auth context
  IF auth.uid() IS NOT NULL THEN
    SELECT id INTO v_lenser_id
    FROM lensers.profiles
    WHERE user_id = auth.uid()
    LIMIT 1;
  END IF;

  INSERT INTO analytics.contact (
    lenser_id,
    name,
    email,
    subject,
    message,
    kvkk_approved,
    ip_address,
    user_agent
  ) VALUES (
    v_lenser_id,
    trim(p_name),
    lower(trim(p_email)),
    trim(p_subject),
    trim(p_message),
    p_kvkk_approved,
    p_ip_address::inet,
    p_user_agent
  );
END;
$$;


ALTER FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") IS 'Inserts a contact form submission into ops.contact. Accessible to anonymous users.';



CREATE OR REPLACE FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, agent_paused)
  VALUES (p_ai_lenser_id, true)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET agent_paused = true,
        updated_at   = now();

  INSERT INTO agents.policy_evaluations (
    ai_lenser_id,
    evaluation_point,
    policy_type,
    verdict,
    reason,
    context,
    evaluated_at
  ) VALUES (
    p_ai_lenser_id,
    'pre_run',
    'pause',
    'deny',
    'agent paused by owner',
    jsonb_build_object('paused_by', auth.uid()),
    now()
  );
END;
$$;


ALTER FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "lenser_id" "uuid", "body" "text", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO battles.comments (battle_id, lenser_id, body)
  VALUES (p_battle_id, v_lenser_id, p_body)
  RETURNING id, battle_id, lenser_id, body, created_at, updated_at;
END;
$$;


ALTER FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text" DEFAULT 'lenser'::"text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "sender_id" "uuid", "sender_handle" "text", "sender_role" "text", "body" "text", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'lensers', 'public'
    AS $$
DECLARE
  v_sender_id uuid;
BEGIN
  v_sender_id := lensers.get_auth_lenser_id();
  IF v_sender_id IS NULL THEN
    RAISE EXCEPTION 'Unauthorized' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO battles.global_messages (battle_id, sender_id, sender_handle, sender_role, body)
  VALUES (p_battle_id, v_sender_id, p_sender_handle, p_sender_role, p_body)
  RETURNING
    battles.global_messages.id,
    battles.global_messages.battle_id,
    battles.global_messages.sender_id,
    battles.global_messages.sender_handle,
    battles.global_messages.sender_role,
    battles.global_messages.body,
    battles.global_messages.created_at;
END;
$$;


ALTER FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") IS 'Posts a chat message. sender_id is resolved from JWT — not accepted from caller.';



CREATE OR REPLACE FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lensers', 'xp', 'battles', 'public'
    AS $$
DECLARE
  v_score     integer := 0;
  v_bio       text;
  v_avatar    text;
  v_location  text;
  v_website   text;
  v_battles   bigint;
  v_total_xp  bigint;
BEGIN
  SELECT
    p.bio,
    p.avatar_url,
    p.location,
    p.website_url
  INTO v_bio, v_avatar, v_location, v_website
  FROM lensers.profiles p
  WHERE p.id = p_lenser_id;

  IF NOT FOUND THEN
    RETURN 0;
  END IF;

  IF v_bio IS NOT NULL AND length(trim(v_bio)) > 0 THEN
    v_score := v_score + 25;
  END IF;
  IF v_avatar IS NOT NULL AND length(trim(v_avatar)) > 0 THEN
    v_score := v_score + 25;
  END IF;
  IF v_location IS NOT NULL AND length(trim(v_location)) > 0 THEN
    v_score := v_score + 15;
  END IF;
  IF v_website IS NOT NULL AND length(trim(v_website)) > 0 THEN
    v_score := v_score + 15;
  END IF;

  -- Battle participation ('cancelled' is not a valid battles.battle_status_enum value)
  SELECT COUNT(*) INTO v_battles
  FROM battles.contenders bc
  JOIN battles.battles b ON b.id = bc.battle_id
  WHERE bc.contender_ref_id = p_lenser_id
    AND b.status <> 'draft'::battles.battle_status_enum;

  IF v_battles > 0 THEN
    v_score := v_score + 10;
  END IF;

  -- XP earned (any positive amount)
  SELECT COALESCE(SUM(xp), 0) INTO v_total_xp
  FROM xp.events
  WHERE lenser_id = p_lenser_id;

  IF v_total_xp > 0 THEN
    v_score := v_score + 10;
  END IF;

  RETURN LEAST(v_score, 100);
END;
$$;


ALTER FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") IS 'Returns a 0–100 profile completion score. Breakdown: bio +25, avatar +25, location +15, website +15, battles >0 +10, XP >0 +10.';



CREATE OR REPLACE FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") RETURNS "agents"."memory_profiles"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run     agents.scratchpad_runs;
  v_profile agents.memory_profiles;
BEGIN
  SELECT * INTO v_run FROM agents.scratchpad_runs WHERE id = p_run_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'scratchpad run not found' USING ERRCODE = 'P0002';
  END IF;

  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_memory_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_run.ai_lenser_id)
     OR NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.memory_profiles
  SET seed = seed || jsonb_build_array(jsonb_build_object(
        'source', 'scratchpad',
        'run_id', v_run.id,
        'prompt', v_run.prompt,
        'output', v_run.output,
        'promoted_at', now()
      )),
      updated_at = now()
  WHERE id = p_memory_profile_id
  RETURNING * INTO v_profile;

  RETURN v_profile;
END;
$$;


ALTER FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'audit', 'extensions', 'public'
    AS $$
DECLARE
  v_lenser_id UUID := lensers.get_auth_lenser_id();
  v_hash TEXT;
BEGIN
  IF NOT EXISTS (
    SELECT 1
    FROM battles.battles
    WHERE id = p_battle_id
      AND creator_lenser_id = v_lenser_id
      AND status = 'draft'::battles.battle_status_enum
      AND deleted_at IS NULL
  ) THEN
    RAISE EXCEPTION 'not_authorized_or_invalid_state' USING ERRCODE = '42501';
  END IF;

  v_hash := public.fn_create_battle_config_snapshot(p_battle_id, 'publish');

  UPDATE battles.battles
     SET status = 'open'::battles.battle_status_enum,
         published_at = now(),
         updated_at = now()
   WHERE id = p_battle_id;

  INSERT INTO audit.attestations (
    entity_type, entity_id, attested_by, attestation_type, proof_payload
  )
  VALUES (
    'battle', p_battle_id, v_lenser_id, 'battle_published',
    jsonb_build_object('snapshot_hash', v_hash, 'battle_id', p_battle_id)
  );

  INSERT INTO battles.events (battle_id, event_type, actor_id, metadata)
  VALUES (
    p_battle_id,
    'published',
    v_lenser_id,
    jsonb_build_object('snapshot_hash', v_hash)
  );

  RETURN jsonb_build_object(
    'snapshot_hash', v_hash,
    'status', 'open',
    'battle_id', p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") IS 'Atomically publishes a battle: freezes rules into rule_snapshots, transitions status to ''open'', and records an audit attestation. Phase A fix: search_path now includes extensions for pgcrypto digest(). Only the battle creator may call this. Battle must be in ''draft'' status. Direct UPDATE on battles.battles.status is restricted to service_role via column-level revoke (see A3 below).';



CREATE OR REPLACE FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  PERFORM lenses.fn_publish_workflow_version(p_version_id);
END;
$$;


ALTER FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") IS 'Public wrapper: transitions a draft version to published and sets head_version_id. Owner-only. Delegates auth + immutability guard to lenses.fn_publish_workflow_version.';



CREATE OR REPLACE FUNCTION "public"."fn_purge_due_accounts"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'analytics'
    AS $$
DECLARE
  v_count   int := 0;
  v_profile RECORD;
BEGIN
  FOR v_profile IN
    SELECT id, user_id, deletion_deadline_at
    FROM lensers.profiles
    WHERE status = 'pending_deletion'
      AND deletion_deadline_at <= now() - interval '1 hour'
  LOOP
    UPDATE lensers.profiles
    SET display_name = 'Deleted User',
        handle       = 'deleted_' || v_profile.id::text,
        bio          = null,
        headline     = null,
        avatar_url   = null,
        banner_url   = null,
        website_url  = null,
        preferences  = null,
        status       = 'deleted',
        updated_at   = now()
    WHERE id = v_profile.id;

    PERFORM lensers.log_account_lifecycle_event(
      v_profile.id,
      v_profile.user_id,
      'deletion_completed',
      'pending_deletion'::lensers.lenser_status,
      'deleted'::lensers.lenser_status,
      'system_job',
      jsonb_build_object(
        'deletion_deadline_at',     v_profile.deletion_deadline_at,
        'purged_after_grace_period', true
      )
    );

    DELETE FROM lensers.relationships
    WHERE source_profile_id = v_profile.id OR target_profile_id = v_profile.id;

    DELETE FROM lensers.social_links WHERE lenser_id = v_profile.id;

    v_count := v_count + 1;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_purge_due_accounts"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_queue_freeze"("p_reason" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_profile_id UUID;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'queue_freeze_forbidden: admin role required';
  END IF;

  SELECT id INTO v_profile_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  UPDATE admin.execution_control
  SET    queue_frozen  = true,
         frozen_reason = p_reason,
         frozen_by     = v_profile_id,
         frozen_at     = now()
  WHERE  id = 1;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'queue.frozen',
    'lenser',
    v_profile_id,
    'warn',
    jsonb_build_object('reason', p_reason)
  );
END;
$$;


ALTER FUNCTION "public"."fn_queue_freeze"("p_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") IS 'Admin-only: freeze the scheduled-workflow dispatch queue. Prevents fn_dispatch_scheduled_workflows from enqueuing new runs without cancelling runs already in flight. Resume with fn_queue_unfreeze.';



CREATE OR REPLACE FUNCTION "public"."fn_queue_unfreeze"() RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'admin', 'public', 'lensers'
    AS $$
DECLARE
  v_profile_id UUID;
BEGIN
  IF NOT public.is_admin() THEN
    RAISE EXCEPTION 'queue_unfreeze_forbidden: admin role required';
  END IF;

  SELECT id INTO v_profile_id
  FROM   lensers.profiles
  WHERE  user_id = auth.uid()
  LIMIT  1;

  UPDATE admin.execution_control
  SET    queue_frozen  = false,
         frozen_reason = NULL,
         frozen_by     = NULL,
         frozen_at     = NULL
  WHERE  id = 1;

  INSERT INTO audit.events (event_type, actor_type, actor_id, severity, payload)
  VALUES (
    'queue.unfrozen',
    'lenser',
    v_profile_id,
    'info',
    jsonb_build_object('resumed_by', v_profile_id)
  );
END;
$$;


ALTER FUNCTION "public"."fn_queue_unfreeze"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_queue_unfreeze"() IS 'Admin-only: resume the scheduled-workflow dispatch queue after a freeze.';



CREATE OR REPLACE FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text" DEFAULT NULL::"text", "p_limit" integer DEFAULT 10, "p_team_run_id" "uuid" DEFAULT NULL::"uuid") RETURNS SETOF "agents"."memories"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_profile agents.memory_profiles;
  v_row     agents.memories;
BEGIN
  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  FOR v_row IN
    SELECT *
    FROM agents.memories
    WHERE profile_id = p_profile_id
      AND is_redacted = false
      AND (p_scope IS NULL OR scope = p_scope)
      AND (expires_at IS NULL OR expires_at > now())
    ORDER BY confidence DESC, created_at DESC
    LIMIT GREATEST(COALESCE(p_limit, 10), 1)
  LOOP
    INSERT INTO agents.memory_access_logs (memory_id, team_run_id, action, context)
    VALUES (v_row.id, p_team_run_id, 'read',
            jsonb_build_object('scope', p_scope, 'profile_id', p_profile_id));
    RETURN NEXT v_row;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_report    agents.run_reports;
  v_id        uuid;
BEGIN
  SELECT * INTO v_report
    FROM agents.run_reports
   WHERE id = p_run_report_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'run report not found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_report.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF p_incident_type NOT IN (
    'tool_failure','budget_exceeded','policy_violation','timeout',
    'step_failure','approval_rejected','kill_switch'
  ) THEN
    RAISE EXCEPTION 'invalid incident_type: %', p_incident_type USING ERRCODE = '22023';
  END IF;

  IF p_severity NOT IN ('low','medium','high','critical') THEN
    RAISE EXCEPTION 'invalid severity: %', p_severity USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.run_incidents (
    run_report_id,
    ai_lenser_id,
    incident_type,
    severity,
    title,
    description,
    context
  ) VALUES (
    p_run_report_id,
    v_report.ai_lenser_id,
    p_incident_type,
    p_severity,
    p_title,
    p_description,
    COALESCE(p_context, '{}'::jsonb)
  )
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb" DEFAULT NULL::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
  v_id        uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_target_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot record provenance for run %', p_target_run_id
      USING ERRCODE = '42501';
  END IF;

  INSERT INTO lenses.workflow_run_provenance (
    source_run_id, source_node_id, source_output_path,
    target_run_id, target_node_id, target_input_path,
    transform
  )
  VALUES (
    p_source_run_id, p_source_node_id, p_source_output_path,
    p_target_run_id, p_target_node_id, p_target_input_path,
    p_transform
  )
  ON CONFLICT (source_run_id, source_node_id, source_output_path,
               target_run_id, target_node_id, target_input_path)
  DO UPDATE SET transform = COALESCE(EXCLUDED.transform, workflow_run_provenance.transform)
  RETURNING id INTO v_id;

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") IS 'Records a field-level data handoff. Idempotent on the (source, target, path) tuple. Transform JSON may carry mapping expression captured at edge-binding time.';



CREATE OR REPLACE FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  IF p_battle_id IS NULL THEN
    RAISE EXCEPTION 'battle_id required' USING ERRCODE = '22023';
  END IF;
  IF p_plugin_id IS NULL OR length(trim(p_plugin_id)) = 0 THEN
    RAISE EXCEPTION 'plugin_id required' USING ERRCODE = '22023';
  END IF;

  INSERT INTO battles.scoring_plugin_signals (battle_id, plugin_id, signals, recorded_at)
  VALUES (p_battle_id, p_plugin_id, COALESCE(p_signals, '{}'::jsonb), now())
  ON CONFLICT (battle_id, plugin_id) DO UPDATE
    SET signals     = EXCLUDED.signals,
        recorded_at = EXCLUDED.recorded_at;
END;
$$;


ALTER FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") IS 'Phase R2: upserts a (battle_id, plugin_id) row in battles.scoring_plugin_signals. Service-role only — plugins run server side under service_role. battles.fn_battles_finalize is intentionally left untouched in this migration.';



CREATE OR REPLACE FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text" DEFAULT NULL::"text", "p_lens_hash" "text" DEFAULT NULL::"text", "p_agent_config_hash" "text" DEFAULT NULL::"text", "p_runner_version" "text" DEFAULT NULL::"text", "p_cli_version" "text" DEFAULT NULL::"text", "p_policy_passed" boolean DEFAULT false) RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'execution', 'public'
    AS $$
  SELECT execution.fn_record_signed_attestation(
    p_run_id, p_envelope_kid, p_envelope_iat, p_envelope_nonce,
    p_canonical_jcs_b64url, p_signature_b64url,
    p_workflow_hash, p_lens_hash, p_agent_config_hash,
    p_runner_version, p_cli_version, p_policy_passed
  );
$$;


ALTER FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_mem agents.memories;
BEGIN
  SELECT * INTO v_mem FROM agents.memories WHERE id = p_memory_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_mem.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.memories
     SET content = '[redacted]',
         is_redacted = true
   WHERE id = p_memory_id;

  INSERT INTO agents.memory_access_logs (memory_id, action, context)
  VALUES (p_memory_id, 'redact', jsonb_build_object('reason', COALESCE(p_reason, '')));
END;
$$;


ALTER FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") OWNER TO "postgres";



-- ── battles.fn_populate_contender_entity_map ─────────────────────────────────

DROP FUNCTION IF EXISTS "battles"."fn_populate_contender_entity_map"();

CREATE FUNCTION "battles"."fn_populate_contender_entity_map"()
  RETURNS "trigger"
  LANGUAGE "plpgsql" SECURITY DEFINER
  SET "search_path" TO 'battles', 'agents'
AS $$
DECLARE
  v_ai_lenser_id uuid;
BEGIN
  -- Resolve contender_ref_id for AI contender types.
  -- Accepts EITHER agents.ai_lensers.id (direct agent UUID — backward-compat)
  -- OR lensers.profiles.id (frontend convention via fn_invite_battle_contender).
  -- A profile-based ref_id is resolved to the canonical agents.ai_lensers.id via fallback.
  IF NEW.contender_type IN ('ai_model', 'ai_agent') THEN
    -- Try direct match: caller already has the agent UUID
    SELECT al.id INTO v_ai_lenser_id
      FROM agents.ai_lensers al
     WHERE al.id = NEW.contender_ref_id;

    -- Fallback: caller supplied the lenser profile UUID
    IF v_ai_lenser_id IS NULL THEN
      SELECT al.id INTO v_ai_lenser_id
        FROM agents.ai_lensers al
       WHERE al.profile_id = NEW.contender_ref_id;
    END IF;
  END IF;

  INSERT INTO battles.contender_entity_map (contender_id, profile_id, ai_lenser_id)
  VALUES (
    NEW.id,
    CASE WHEN NEW.contender_type = 'human' THEN NEW.contender_ref_id ELSE NULL END,
    v_ai_lenser_id   -- NULL for human; resolved agent UUID for AI types
  )
  ON CONFLICT (contender_id) DO NOTHING;

  RETURN NEW;
END;
$$;

COMMENT ON FUNCTION "battles"."fn_populate_contender_entity_map"() IS
  'AFTER INSERT trigger on battles.contenders. Populates battles.contender_entity_map. '
  'For AI contender types (ai_model, ai_agent): resolves contender_ref_id to the canonical '
  'agents.ai_lensers.id, accepting either the agent UUID directly (backward-compat) or the '
  'lenser profile UUID (frontend convention via fn_invite_battle_contender).';

ALTER FUNCTION "battles"."fn_populate_contender_entity_map"() OWNER TO "postgres";

GRANT ALL ON FUNCTION "battles"."fn_populate_contender_entity_map"() TO "service_role";


CREATE OR REPLACE FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses', 'extensions'
    AS $$
DECLARE
  v_snapshot JSONB;
BEGIN
  SELECT jsonb_build_object(
    'agent_id', al.id,
    'profile_id', al.profile_id,
    'handle', p.handle,
    'display_name', p.display_name,
    'runtime_pref', al.runtime_pref,
    'is_active', al.is_active,
    'suspended', al.suspended_at IS NOT NULL,
    'personality_note_hash', CASE
      WHEN al.personality_note IS NULL THEN NULL
      ELSE encode(extensions.digest(al.personality_note, 'sha256'), 'hex')
    END,
    'policies', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', pol.id,
        'can_join_battles', pol.can_join_battles,
        'can_vote', pol.can_vote,
        'can_create_battles', pol.can_create_battles,
        'can_receive_sponsorship', pol.can_receive_sponsorship,
        'model_binding_mode', pol.model_binding_mode,
        'max_daily_battles', pol.max_daily_battles,
        'max_daily_votes', pol.max_daily_votes,
        'allowed_battle_types', pol.allowed_battle_types,
        'spending_limit_credits', pol.spending_limit_credits,
        'allowed_output_modalities', pol.allowed_output_modalities,
        'is_public_policy', pol.is_public_policy
      ) ORDER BY pol.created_at)
      FROM agents.policies pol
      WHERE pol.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'lens_bindings', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', lb.id,
        'lens_id', lb.lens_id,
        'version_id', COALESCE(lb.version_id, l.head_version_id),
        'is_default', lb.is_default,
        'category_tags', lb.category_tags
      ) ORDER BY lb.created_at)
      FROM agents.lens_bindings lb
      JOIN lenses.lenses l ON l.id = lb.lens_id
      WHERE lb.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'model_bindings', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', mb.id,
        'model_id', mb.model_id,
        'is_default', mb.is_default,
        'category_tags', mb.category_tags,
        'byok_adapter', mb.byok_adapter,
        'ollama_endpoint_hash', CASE
          WHEN mb.ollama_endpoint IS NULL THEN NULL
          ELSE encode(extensions.digest(mb.ollama_endpoint, 'sha256'), 'hex')
        END
      ) ORDER BY mb.created_at)
      FROM agents.model_bindings mb
      WHERE mb.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'tool_profiles', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', tp.id,
        'allow_tools', tp.allow_tools,
        'deny_tools', tp.deny_tools,
        'tool_groups', tp.tool_groups,
        'requires_approval', tp.requires_approval,
        'is_default', tp.is_default
      ) ORDER BY tp.created_at)
      FROM agents.tool_profiles tp
      WHERE tp.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'tool_assignments', COALESCE((
      SELECT jsonb_agg(jsonb_build_object(
        'id', ta.id,
        'tool_id', ta.tool_id,
        'profile_id', ta.profile_id,
        'allowed', ta.allowed
      ) ORDER BY ta.created_at)
      FROM agents.tool_assignments ta
      WHERE ta.ai_lenser_id = al.id
    ), '[]'::jsonb),
    'memory_policy', jsonb_build_object(
      'memory_profile_count', (
        SELECT count(*) FROM agents.memory_profiles mp WHERE mp.ai_lenser_id = al.id
      ),
      'memory_body_included', false
    ),
    'redaction', jsonb_build_object(
      'secrets_included', false,
      'oauth_tokens_included', false,
      'api_keys_included', false,
      'private_memory_bodies_included', false
    )
  )
  INTO v_snapshot
  FROM agents.ai_lensers al
  JOIN lensers.profiles p ON p.id = al.profile_id
  WHERE al.id = p_ai_lenser_id;

  IF v_snapshot IS NULL THEN
    RAISE EXCEPTION 'agent_not_found: %', p_ai_lenser_id USING ERRCODE = 'P0002';
  END IF;

  RETURN v_snapshot;
END;
$$;


ALTER FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") RETURNS "text"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'extensions'
    AS $$
  SELECT encode(extensions.digest(public.fn_redacted_agent_snapshot(p_ai_lenser_id)::text, 'sha256'), 'hex');
$$;


ALTER FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text" DEFAULT NULL::"text", "p_category" "text" DEFAULT 'general'::"text", "p_schema_input" "jsonb" DEFAULT '{}'::"jsonb", "p_schema_output" "jsonb" DEFAULT '{}'::"jsonb", "p_auth_method" "text" DEFAULT 'none'::"text", "p_requires_approval" boolean DEFAULT false, "p_is_dangerous" boolean DEFAULT false) RETURNS "agents"."tools_registry"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_owner uuid := lensers.get_auth_human_lenser_id();
  v_row   agents.tools_registry;
BEGIN
  IF v_owner IS NULL THEN
    RAISE EXCEPTION 'authentication required or no human lenser profile found'
      USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.tools_registry (
    owner_lenser_id, key, name, description, category,
    schema_input, schema_output, auth_method, requires_approval, is_dangerous
  )
  VALUES (
    v_owner, p_key, p_name, p_description, COALESCE(p_category, 'general'),
    COALESCE(p_schema_input,      '{}'::jsonb),
    COALESCE(p_schema_output,     '{}'::jsonb),
    COALESCE(p_auth_method,       'none'),
    COALESCE(p_requires_approval, false),
    COALESCE(p_is_dangerous,      false)
  )
  ON CONFLICT (owner_lenser_id, key)
  DO UPDATE SET
    name              = EXCLUDED.name,
    description       = EXCLUDED.description,
    category          = EXCLUDED.category,
    schema_input      = EXCLUDED.schema_input,
    schema_output     = EXCLUDED.schema_output,
    auth_method       = EXCLUDED.auth_method,
    requires_approval = EXCLUDED.requires_approval,
    is_dangerous      = EXCLUDED.is_dangerous,
    updated_at        = now()
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS "battles"."tournament_contenders"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_row       battles.tournament_contenders;
  v_count     INT;
  v_max       INT;
BEGIN
  v_lenser_id := COALESCE(p_lenser_id, auth.uid());

  SELECT max_contenders INTO v_max FROM battles.tournaments WHERE id = p_tournament_id;
  SELECT COUNT(*) INTO v_count FROM battles.tournament_contenders WHERE tournament_id = p_tournament_id;

  IF v_count >= v_max THEN
    RAISE EXCEPTION 'Tournament is full (% max contenders)', v_max;
  END IF;

  INSERT INTO battles.tournament_contenders (tournament_id, lenser_id)
  VALUES (p_tournament_id, v_lenser_id)
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_target_id uuid;
BEGIN
  SELECT id INTO v_target_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_target_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.relationships
  SET status = 'rejected',
      responded_at = now()
  WHERE source_profile_id = p_source_profile_id
    AND target_profile_id = v_target_id
    AND status = 'pending';

  IF NOT FOUND THEN
    RETURN jsonb_build_object('success', false, 'reason', 'no_pending_request');
  END IF;

  RETURN jsonb_build_object('success', true);
END;
$$;


ALTER FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_inv agents.tool_invocations;
BEGIN
  SELECT * INTO v_inv FROM agents.tool_invocations WHERE id = p_invocation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'invocation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_inv.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  IF v_inv.approval_status <> 'pending' THEN
    RAISE EXCEPTION 'invocation is not pending approval' USING ERRCODE = '22023';
  END IF;

  UPDATE agents.tool_invocations
     SET approval_status     = 'rejected',
         approval_decided_by = auth.uid(),
         approval_reason     = p_reason,
         status              = 'rejected',
         completed_at        = now()
   WHERE id = p_invocation_id;
END;
$$;


ALTER FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM content.threads
    WHERE id = p_thread_id AND lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RETURN;
  END IF;

  DELETE FROM content.tag_map
  WHERE entity_type::text = 'thread' AND entity_id = p_thread_id;

  IF p_tag_ids IS NOT NULL AND array_length(p_tag_ids, 1) > 0 THEN
    INSERT INTO content.tag_map (entity_type, entity_id, tag_id)
    SELECT 'thread', p_thread_id, unnest(p_tag_ids)
    ON CONFLICT DO NOTHING;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) IS 'Security wrapper: atomically replace all tag mappings for a thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  DELETE FROM battles.contenders c
  USING battles.battles b
  WHERE c.id = p_contender_id
    AND c.battle_id = b.id
    AND b.creator_lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id uuid;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  UPDATE lensers.relationships
  SET status = 'removed',
      removed_at = now()
  WHERE source_profile_id = v_source_id
    AND target_profile_id = p_target_profile_id
    AND status IN ('accepted', 'pending');

  RETURN jsonb_build_object('following', false);
END;
$$;


ALTER FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_id   uuid;
  v_idx  integer := 0;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'reorder_phases_forbidden' USING ERRCODE = '42501';
  END IF;

  FOREACH v_id IN ARRAY p_ordered_ids LOOP
    UPDATE lenses.workflow_phases SET ordinal = v_idx WHERE id = v_id AND workflow_id = p_workflow_id;
    v_idx := v_idx + 1;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_id   uuid;
  v_idx  integer := 0;
BEGIN
  -- Verify ownership via phase → workflow
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflow_phases wp
    JOIN lenses.workflows w ON w.id = wp.workflow_id
    WHERE wp.id = p_phase_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'reorder_tasks_forbidden' USING ERRCODE = '42501';
  END IF;

  FOREACH v_id IN ARRAY p_ordered_ids LOOP
    UPDATE lenses.workflow_tasks SET ordinal = v_idx WHERE id = v_id AND phase_id = p_phase_id;
    v_idx := v_idx + 1;
  END LOOP;
END;
$$;


ALTER FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id     uuid;
  v_target        RECORD;
  v_existing      lensers.relationship_status;
  v_new_status    lensers.relationship_status;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required or profile not found';
  END IF;

  IF v_source_id = p_target_profile_id THEN
    RAISE EXCEPTION 'Cannot follow yourself';
  END IF;

  -- Check target exists and is active
  SELECT p.id, p.status, p.visibility INTO v_target
  FROM lensers.profiles p WHERE p.id = p_target_profile_id;

  IF NOT FOUND OR v_target.status NOT IN ('active') THEN
    RETURN jsonb_build_object('status', 'rejected', 'reason', 'target_unavailable');
  END IF;

  -- Check if blocked in either direction
  IF EXISTS (
    SELECT 1 FROM lensers.relationships
    WHERE status = 'blocked'
      AND (
        (source_profile_id = v_source_id AND target_profile_id = p_target_profile_id)
        OR (source_profile_id = p_target_profile_id AND target_profile_id = v_source_id)
      )
  ) THEN
    RETURN jsonb_build_object('status', 'rejected', 'reason', 'blocked');
  END IF;

  -- Check existing relationship
  SELECT r.status INTO v_existing
  FROM lensers.relationships r
  WHERE r.source_profile_id = v_source_id AND r.target_profile_id = p_target_profile_id;

  IF v_existing = 'accepted' THEN
    RETURN jsonb_build_object('status', 'accepted', 'reason', 'already_following');
  END IF;

  IF v_existing = 'pending' THEN
    RETURN jsonb_build_object('status', 'pending', 'reason', 'already_pending');
  END IF;

  -- Determine status based on target visibility
  IF v_target.visibility IN ('public', 'community') THEN
    v_new_status := 'accepted';
  ELSE
    v_new_status := 'pending';
  END IF;

  INSERT INTO lensers.relationships (source_profile_id, target_profile_id, status, accepted_at)
  VALUES (
    v_source_id,
    p_target_profile_id,
    v_new_status,
    CASE WHEN v_new_status = 'accepted' THEN now() ELSE null END
  )
  ON CONFLICT (source_profile_id, target_profile_id)
  DO UPDATE SET
    status       = v_new_status,
    requested_at = now(),
    responded_at = null,
    accepted_at  = CASE WHEN v_new_status = 'accepted' THEN now() ELSE null END,
    removed_at   = null;

  RETURN jsonb_build_object('status', v_new_status::text);
END;
$$;


ALTER FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text" DEFAULT NULL::"text") RETURNS "agents"."workspace_settings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.workspace_settings;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id)
  VALUES (p_ai_lenser_id)
  ON CONFLICT (ai_lenser_id) DO NOTHING;

  UPDATE agents.workspace_settings
  SET metadata = metadata || jsonb_build_object(
        'deletion_requested_at', now(),
        'deletion_reason',       p_reason
      ),
      updated_at = now()
  WHERE ai_lenser_id = p_ai_lenser_id
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_requeue_battle_job_with_backoff(p_job_id, p_backoff_ms, p_error);
END;
$$;


ALTER FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text" DEFAULT NULL::"text", "p_model_override" "text" DEFAULT NULL::"text") RETURNS TABLE("model_id" "uuid", "model_key" "text", "provider_id" "uuid", "provider_key" "text")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
DECLARE
  v_provider_id uuid;
BEGIN
  -- Resolve optional provider override to a provider_id
  IF p_provider_override IS NOT NULL THEN
    SELECT p.id INTO v_provider_id
    FROM ai.providers p
    WHERE p.key = p_provider_override
    LIMIT 1;
  END IF;

  RETURN QUERY
  SELECT m.id, m.key, m.provider_id,
         (SELECT pr.key FROM ai.providers pr WHERE pr.id = m.provider_id LIMIT 1)
  FROM ai.models m
  WHERE m.is_active = true
    AND (v_provider_id IS NULL OR m.provider_id = v_provider_id)
    AND (
      p_model_override IS NULL
      OR m.id::text = p_model_override
      OR m.key      = p_model_override
    )
  ORDER BY m.created_at ASC
  LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") IS 'Security wrapper: resolve the best active AI model, optionally filtered by provider key and/or model key/id. Returns model_id, model_key, provider_id, provider_key.';



CREATE OR REPLACE FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $_$
DECLARE
  v_normalized text;
  v_user_id    uuid;
  v_email      text;
BEGIN
  -- 1. Normalise: strip leading @, trim whitespace, fold to lowercase
  v_normalized := lower(trim(regexp_replace(p_handle, '^@+', '')));

  -- 2. Reject inputs that cannot possibly be a stored handle
  --    (stored format: ^[a-z0-9._]{4,24}$)
  IF v_normalized !~ '^[a-z0-9._]{4,24}$' THEN
    RETURN NULL;
  END IF;

  -- 3. Look up the lenser profile — human accounts only, not deleted
  SELECT user_id
    INTO v_user_id
    FROM lensers.profiles
   WHERE handle = v_normalized
     AND type   = 'human'
     AND status NOT IN ('deleted'::lenser_status, 'pending_deletion'::lenser_status)
   LIMIT 1;

  IF v_user_id IS NULL THEN
    RETURN NULL;
  END IF;

  -- 4. Fetch the email from auth.users
  SELECT email
    INTO v_email
    FROM auth.users
   WHERE id         = v_user_id
     AND deleted_at IS NULL
   LIMIT 1;

  -- Returns NULL when auth record is gone (orphaned profile)
  RETURN v_email;
END;
$_$;


ALTER FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") IS 'Resolves a profile handle (username) to the associated auth.users email. Used exclusively by login flows to support handle+password authentication. Returns NULL for missing, deleted, or non-human handles. SECURITY: never surface the return value or its NULL state to end users; always return a generic "Invalid login credentials" error to callers.';



CREATE OR REPLACE FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) RETURNS TABLE("id" "uuid", "title" "text", "link" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'content', 'lensers', 'auth'
    AS $$
  SELECT
    l.id,
    COALESCE(t.title, '') AS title,
    '/lenses/' || l.id::text AS link
  FROM lenses.lenses l
  LEFT JOIN content.entity_translations t
    ON  t.entity_id   = l.id
    AND t.entity_type = 'lens'
    AND t.is_original = true
  WHERE l.id = ANY(p_ids)
    AND (
      l.visibility = 'public'
      OR l.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) IS 'Batch-resolves lens mention IDs to (id, title, link). Caller sees their own private lenses; other private lenses are excluded. Replaces N+1 getLensDetail calls in mentionService.';



CREATE OR REPLACE FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
  UPDATE agents.run_incidents ri
  SET resolved_at = now(), resolution = p_resolution
  FROM agents.run_reports rr
  WHERE ri.id = p_incident_id
    AND ri.run_report_id = rr.id
    AND rr.ai_lenser_id IN (
      SELECT o.ai_lenser_id FROM agents.ownerships o
      WHERE o.owner_lenser_id = lensers.get_auth_lenser_id()
        AND o.revoked_at IS NULL
    );
$$;


ALTER FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'audit', 'public'
    AS $$
DECLARE
  v_actor UUID;
BEGIN
  v_actor := auth.uid();
  IF v_actor IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'P0001';
  END IF;

  IF NOT public.fn_is_super_admin() THEN
    RAISE EXCEPTION 'Forbidden: super_admin required' USING ERRCODE = 'P0003';
  END IF;

  UPDATE audit.vote_anomalies
     SET resolved_at  = now(),
         resolved_by  = v_actor
   WHERE id = p_anomaly_id
     AND resolved_at IS NULL;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Anomaly not found or already resolved' USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") IS 'CA: Marks an anomaly resolved. Requires super_admin.';



CREATE OR REPLACE FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('agent', p_ai_lenser_id); $$;


ALTER FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('lens', p_lens_id); $$;


ALTER FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$ SELECT public.fn_artifact_restore('workflow', p_workflow_id); $$;


ALTER FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  PERFORM lenses.fn_restore_workflow_version(p_version_id);
END;
$$;


ALTER FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") IS 'Public wrapper: copies a historical version DAG back into the live workflow nodes/edges as a new draft without mutating the historical version. Owner-only. Delegates auth check to lenses.fn_restore_workflow_version.';



CREATE OR REPLACE FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, agent_paused)
  VALUES (p_ai_lenser_id, false)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET agent_paused = false,
        updated_at   = now();
END;
$$;


ALTER FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run     agents.team_runs;
  v_new_id  uuid;
BEGIN
  SELECT * INTO v_run
    FROM agents.team_runs
   WHERE id = p_run_id
   FOR UPDATE;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'team run not found' USING ERRCODE = 'P0002';
  END IF;

  IF v_run.ai_lenser_id <> p_ai_lenser_id THEN
    RAISE EXCEPTION 'run does not belong to this agent' USING ERRCODE = '42501';
  END IF;

  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  IF v_run.status NOT IN ('failed', 'cancelled', 'completed') THEN
    RAISE EXCEPTION 'only terminal runs can be retried' USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.team_runs (
    ai_lenser_id,
    team_id,
    workflow_id,
    workflow_assignment_id,
    status,
    approval_status,
    metadata
  ) VALUES (
    v_run.ai_lenser_id,
    v_run.team_id,
    v_run.workflow_id,
    v_run.workflow_assignment_id,
    'queued',
    'pending',
    jsonb_build_object('retried_from', v_run.id)
  )
  RETURNING id INTO v_new_id;

  INSERT INTO agents.agent_run_events (
    team_run_id,
    event_type,
    payload,
    occurred_at
  ) VALUES (
    v_new_id,
    'run_retried',
    jsonb_build_object(
      'original_run_id', v_run.id,
      'retried_by',      auth.uid()
    ),
    now()
  );

  RETURN v_new_id::text;
END;
$$;


ALTER FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") IS 'Clone a terminal (failed/cancelled/completed) team_run as a new queued run. Returns the new run id. Authorization via agents.can_manage_ai_lenser.';



CREATE OR REPLACE FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_retry_dead_letter_battle_job(p_dead_letter_id);
END;
$$;


ALTER FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_ownership agents.ownerships%ROWTYPE;
BEGIN
  SELECT *
  INTO v_ownership
  FROM agents.ownerships o
  WHERE o.id = p_ownership_id
  LIMIT 1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Ownership grant not found.'
      USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_ownership.ai_lenser_id) THEN
    RAISE EXCEPTION 'Not authorized to revoke this delegate.'
      USING ERRCODE = '42501';
  END IF;

  IF v_ownership.role = 'owner' THEN
    RAISE EXCEPTION 'The primary owner cannot be revoked through this RPC.'
      USING ERRCODE = '22023';
  END IF;

  UPDATE agents.ownerships
  SET revoked_at = now()
  WHERE id = p_ownership_id;
END;
$$;


ALTER FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") IS 'Revokes a co_owner or operator delegation for an AI Lenser. The primary owner must be handled through a dedicated ownership-transfer flow.';



CREATE OR REPLACE FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
BEGIN
    PERFORM ai.fn_revoke_api_key(p_key_id => p_key_id);
END;
$$;


ALTER FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") IS 'Public REST wrapper for ai.fn_revoke_api_key. Soft-revokes a BYOK API key — sets is_active=false, records revoked_at. Only the key owner can revoke (enforced inside ai.fn_revoke_api_key). The ai schema is not exposed in PostgREST — this wrapper is the only REST-callable entry point for key revocation. Added in migration 000057.';



CREATE OR REPLACE FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_caller       uuid := lensers.get_auth_lenser_id();
  v_ai_lenser_id uuid;
BEGIN
  IF v_caller IS NULL THEN
    RAISE EXCEPTION 'not_authenticated' USING ERRCODE = '42501';
  END IF;

  SELECT ai_lenser_id INTO v_ai_lenser_id
  FROM   agents.standing_approvals
  WHERE  id = p_id;

  IF v_ai_lenser_id IS NULL THEN
    RAISE EXCEPTION 'standing_approval_not_found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT agents.can_manage_ai_lenser(v_ai_lenser_id) THEN
    RAISE EXCEPTION 'not_owner_of_ai_lenser' USING ERRCODE = '42501';
  END IF;

  UPDATE agents.standing_approvals
  SET    revoked_at = now()
  WHERE  id = p_id
    AND  revoked_at IS NULL;

  INSERT INTO agents.action_logs (
    ai_lenser_id, action_type, result, metadata
  ) VALUES (
    v_ai_lenser_id,
    'standing_approval_revoked',
    'success',
    jsonb_build_object(
      'standing_approval_id', p_id,
      'revoked_by',           v_caller
    )
  );
END;
$$;


ALTER FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") IS 'Phase Q2: revokes a standing approval (sets revoked_at=now()). Owner-only; writes an action_logs entry.';



CREATE OR REPLACE FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;
  DELETE FROM agents.tool_assignments
   WHERE ai_lenser_id = p_ai_lenser_id
     AND tool_id      = p_tool_id;
  RETURN true;
END;
$$;


ALTER FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_rls_unprotected_tables"() RETURNS TABLE("schema_name" "text", "table_name" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT n.nspname::text, c.relname::text
  FROM pg_catalog.pg_class c
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
  WHERE c.relkind = 'r'
    AND n.nspname IN (
      'agents', 'lenses', 'battles', 'workflows',
      'audit', 'execution', 'devices', 'connectors'
    )
    AND NOT c.relrowsecurity
  ORDER BY n.nspname, c.relname;
$$;


ALTER FUNCTION "public"."fn_rls_unprotected_tables"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_rls_unprotected_tables"() IS 'BY: Returns tables in security-sensitive schemas that have relrowsecurity=false. Used by lf security rls-audit CLI command. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_eval  agents.evaluations;
  v_run   agents.evaluation_runs;
BEGIN
  SELECT * INTO v_eval FROM agents.evaluations WHERE id = p_evaluation_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'evaluation not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT (v_eval.owner_lenser_id = auth.uid()
          OR (v_eval.ai_lenser_id IS NOT NULL AND agents.can_manage_ai_lenser(v_eval.ai_lenser_id))) THEN
    RAISE EXCEPTION 'not authorized' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.evaluation_runs (evaluation_id, model_id, status)
  VALUES (p_evaluation_id, p_model_id, 'queued')
  RETURNING * INTO v_run;

  RETURN v_run.id;
END;
$$;


ALTER FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_funding_source" "text" DEFAULT 'platform_credit'::"text", "p_byok_key_id" "uuid" DEFAULT NULL::"uuid", "p_idempotency_key" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lenses', 'lensers'
    AS $$
DECLARE
  v_run_id uuid;
BEGIN
  SELECT execution.fn_run_lens_api(
    p_lens_id, p_version_id, p_model_id, p_inputs,
    p_funding_source, p_byok_key_id, p_idempotency_key
  ) INTO v_run_id;
  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") IS 'Security wrapper: queue a lens execution. Delegates to execution.fn_run_lens_api.';



CREATE OR REPLACE FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'devices', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_binding_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM execution.runners r
    WHERE r.id = p_runner_id AND r.lenser_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM devices.registered_devices d
    WHERE d.id = p_device_id
      AND d.lenser_id = v_lenser_id
      AND d.trust_level IN ('approved','trusted')
  ) THEN
    RAISE EXCEPTION 'device_not_found_or_not_trusted' USING ERRCODE = '42501';
  END IF;

  INSERT INTO execution.runner_device_bindings (runner_id, device_id, status)
  VALUES (p_runner_id, p_device_id, 'active')
  ON CONFLICT (runner_id, device_id) DO UPDATE
    SET status = 'active', bound_at = now()
  RETURNING id INTO v_binding_id;

  RETURN v_binding_id;
END;
$$;


ALTER FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  UPDATE execution.runners
     SET is_active = true,
         updated_at = now()
   WHERE id = p_adapter_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") RETURNS TABLE("id" "uuid", "name" "text", "adapter_type" "text", "is_active" boolean, "config" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "last_probed_at" timestamp with time zone, "last_probe_status" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT r.id, r.name, r.adapter_type, r.is_active, r.config, r.created_at,
         r.updated_at, r.last_probed_at, r.last_probe_status
    FROM execution.runners r
   WHERE r.id = p_adapter_id
     AND r.lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_list"() RETURNS TABLE("id" "uuid", "name" "text", "adapter_type" "text", "is_active" boolean, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid()
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'auth_required' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  SELECT r.id, r.name, r.adapter_type, r.is_active, r.created_at
    FROM execution.runners r
   WHERE r.lenser_id = v_lenser_id
   ORDER BY r.created_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_runner_list"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text" DEFAULT 'Hello, are you available?'::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_runner    execution.runners%ROWTYPE;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  SELECT * INTO v_runner
    FROM execution.runners
   WHERE id = p_adapter_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;

  IF NOT v_runner.is_active THEN
    RAISE EXCEPTION 'runner_inactive' USING ERRCODE = 'P0001';
  END IF;

  UPDATE execution.runners
     SET last_probed_at = now(),
         last_probe_status = 'queued',
         updated_at = now()
   WHERE id = p_adapter_id;

  RETURN jsonb_build_object(
    'runner_id',     v_runner.id,
    'adapter_type',  v_runner.adapter_type,
    'probe_status',  'queued',
    'response',      'Probe queued; daemon will execute when reachable.',
    'note',          'Network probes are performed by lf-gatewayd, not the database.'
  );
END;
$$;


ALTER FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
  v_runner_id UUID;
BEGIN
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = auth.uid() AND status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  IF p_adapter_type NOT IN ('openai-agents','langchain','crewai','mcp','ollama','http','custom') THEN
    RAISE EXCEPTION 'invalid_adapter_type: %', p_adapter_type USING ERRCODE = '22023';
  END IF;

  INSERT INTO execution.runners (lenser_id, name, adapter_type, config)
  VALUES (v_lenser_id, p_name, p_adapter_type, COALESCE(p_config, '{}'))
  RETURNING id INTO v_runner_id;

  RETURN v_runner_id;
END;
$$;


ALTER FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution', 'lensers', 'extensions'
    AS $$
DECLARE
  v_lenser_id UUID;
BEGIN
  SELECT lp.id INTO v_lenser_id
  FROM lensers.profiles lp
  WHERE lp.user_id = auth.uid() AND lp.status = 'active'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'active_lenser_required' USING ERRCODE = '42501';
  END IF;

  -- Soft-deactivate (preserve audit trail).
  UPDATE execution.runners
     SET is_active = false,
         updated_at = now()
   WHERE id = p_adapter_id
     AND lenser_id = v_lenser_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'runner_not_found' USING ERRCODE = 'P0002';
  END IF;

  -- Pause any active device bindings; do not delete (keeps history intact).
  UPDATE execution.runner_device_bindings
     SET status = 'paused'
   WHERE runner_id = p_adapter_id
     AND status = 'active';
END;
$$;


ALTER FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  v_lenser_id := lensers.get_auth_lenser_id();

  -- Guard: caller must own the workflow that contains this node
  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_nodes n
    JOIN lenses.workflows w ON w.id = n.workflow_id
    WHERE n.id = p_node_id
      AND w.lenser_id = v_lenser_id
  ) THEN
    RAISE EXCEPTION 'node_not_found_or_forbidden';
  END IF;

  UPDATE lenses.workflow_nodes
  SET config = p_config
  WHERE id = p_node_id;
END;
$$;


ALTER FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_schedule_account_deletion"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_uid uuid;
  v_deadline timestamptz;
  v_profile RECORD;
BEGIN
  v_uid := auth.uid();
  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT id, user_id, status
  INTO v_profile
  FROM lensers.profiles
  WHERE user_id = v_uid
  FOR UPDATE;

  IF NOT FOUND OR v_profile.status NOT IN ('active', 'deactivated') THEN
    RETURN jsonb_build_object('success', false, 'reason', 'not_eligible');
  END IF;

  v_deadline := now() + interval '30 days';

  -- Step 1: set the deletion timestamps. Existing trigger may temporarily move
  -- active profiles to `deactivated`, so we normalize status in step 2.
  UPDATE lensers.profiles
  SET deletion_requested_at = COALESCE(deletion_requested_at, now()),
      deletion_deadline_at = v_deadline,
      updated_at = now()
  WHERE id = v_profile.id;

  -- Step 2: persist the intended grace-period state used by purge + auth flow.
  UPDATE lensers.profiles
  SET status = 'pending_deletion',
      deletion_deadline_at = v_deadline,
      updated_at = now()
  WHERE id = v_profile.id;

  PERFORM lensers.log_account_lifecycle_event(
    v_profile.id,
    v_profile.user_id,
    'deletion_scheduled',
    v_profile.status,
    'pending_deletion'::lensers.lenser_status,
    'user_action',
    jsonb_build_object('deletion_deadline_at', v_deadline)
  );

  RETURN jsonb_build_object('success', true, 'status', 'pending_deletion', 'deadline', v_deadline);
END;
$$;


ALTER FUNCTION "public"."fn_schedule_account_deletion"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer DEFAULT 24, "p_auto_publish" boolean DEFAULT true) RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  IF p_execution_starts_at IS NULL THEN
    RAISE EXCEPTION 'execution_starts_at must not be null.' USING ERRCODE = '22000';
  END IF;

  IF p_execution_starts_at <= now() THEN
    RAISE EXCEPTION 'execution_starts_at must be in the future (got %).',
      p_execution_starts_at USING ERRCODE = '22000';
  END IF;

  RETURN QUERY
  UPDATE battles.battles AS b
  SET
    execution_starts_at   = p_execution_starts_at,
    voting_duration_hours = COALESCE(p_voting_duration_hours, 24),
    auto_publish          = COALESCE(p_auto_publish, true)
  WHERE b.id = p_battle_id
    AND b.creator_lenser_id = v_lenser_id
  RETURNING
    b.id, b.slug, b.title, b.task_prompt, b.status::text, b.total_vote_count, b.published_at,
    b.voting_opens_at, b.voting_closes_at, b.battle_type::text, b.voter_eligibility::text, b.handicap_config,
    b.creator_lenser_id, b.forum_thread_id::text, b.workflow_id, b.lens_id,
    b.execution_starts_at, b.auto_publish, b.voting_duration_hours, b.vote_velocity, b.og_image_url,
    b.winner_contender_id, b.parent_battle_id, b.deleted_at;
END;
$$;


ALTER FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) IS 'Schedule a battle execution time. Rejects past timestamps server-side (issue #208). Casts enum and uuid-as-text columns to text to match declared return type.';



CREATE OR REPLACE FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer DEFAULT 8) RETURNS TABLE("id" "uuid", "handle" "text", "display_name" "text", "avatar_url" "text", "type" "lensers"."lenser_type")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lensers', 'public'
    AS $$
  SELECT
    p.id,
    p.handle,
    p.display_name,
    p.avatar_url,
    p.type
  FROM lensers.profiles p
  WHERE
    p.status                = 'active'
    AND p.deletion_requested_at IS NULL
    AND (
      p.visibility::text = 'public'
      OR (p.visibility::text = 'community' AND lensers.get_auth_lenser_id() IS NOT NULL)
      OR p.id = lensers.get_auth_lenser_id()
    )
    AND (
      CASE
        WHEN left(p_query, 1) = '@' THEN
          p.handle ILIKE '%' || substring(p_query FROM 2) || '%'
        ELSE
          p.handle       ILIKE '%' || p_query || '%'
          OR p.display_name ILIKE '%' || p_query || '%'
      END
    )
  ORDER BY
    CASE
      WHEN left(p_query, 1) = '@' THEN
        CASE
          WHEN p.handle = lower(substring(p_query FROM 2))              THEN 0
          WHEN p.handle ILIKE lower(substring(p_query FROM 2)) || '%'   THEN 1
          ELSE 2
        END
      ELSE
        CASE
          WHEN p.handle = lower(p_query)     THEN 0
          WHEN p.handle ILIKE p_query || '%' THEN 1
          ELSE 2
        END
    END,
    p.handle
  LIMIT LEAST(COALESCE(p_limit, 8), 20);
$$;


ALTER FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) IS 'Search lensers by handle or display_name for battle invite / contender picker. Returns lenser type (human|ai) for wizard UI. Respects profile visibility: public=all, community=authenticated, private=self only. Prefix @ to restrict to handle-only search. LIMIT capped at 20. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "profile_id" "uuid", "ai_lenser_id" "uuid", "scope" "text", "source" "text", "content" "text", "confidence" numeric, "is_redacted" boolean, "created_at" timestamp with time zone, "rank" real)
    LANGUAGE "plpgsql"
    SET "search_path" TO 'agents', 'public'
    AS $$
DECLARE
  v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 20), 1), 50);
  v_query tsquery;
BEGIN
  IF p_query IS NULL OR length(trim(p_query)) = 0 THEN
    RETURN;
  END IF;

  v_query := plainto_tsquery('english', p_query);

  IF v_query::text = '' THEN
    -- The query string contained only stopwords; nothing to search for.
    RETURN;
  END IF;

  RETURN QUERY
    SELECT
      m.id,
      m.profile_id,
      m.ai_lenser_id,
      m.scope,
      m.source,
      m.content,
      m.confidence,
      m.is_redacted,
      m.created_at,
      ts_rank(to_tsvector('english', m.content), v_query) AS rank
    FROM agents.memories m
    WHERE
      (p_profile_id IS NULL OR m.profile_id = p_profile_id)
      AND m.is_redacted = false
      AND to_tsvector('english', m.content) @@ v_query
    ORDER BY rank DESC, m.created_at DESC
    LIMIT v_limit;
END;
$$;


ALTER FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) IS 'Phase N1: Full-text search over agents.memories.content using a plain english tsquery. Honors RLS via SECURITY INVOKER. Returns up to 50 rows ranked by ts_rank.';



CREATE OR REPLACE FUNCTION "public"."fn_security_definer_no_search_path"() RETURNS TABLE("schema_name" "text", "function_name" "text", "full_signature" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT
    n.nspname::text                          AS schema_name,
    p.proname::text                          AS function_name,
    pg_catalog.pg_get_function_identity_arguments(p.oid)::text AS full_signature
  FROM pg_catalog.pg_proc p
  JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
  WHERE p.prosecdef = true                          -- SECURITY DEFINER
    AND n.nspname IN ('public', 'agents', 'lenses',
                      'battles', 'audit', 'execution')
    AND NOT EXISTS (
      SELECT 1 FROM pg_catalog.pg_options_to_table(p.proconfig) AS opt(option_name, option_value)
      WHERE opt.option_name = 'search_path'
    )
  ORDER BY n.nspname, p.proname;
$$;


ALTER FUNCTION "public"."fn_security_definer_no_search_path"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_security_definer_no_search_path"() IS 'BY: Returns SECURITY DEFINER functions that have no SET search_path configured. Used by lf security rls-audit CLI command. SECURITY DEFINER; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_score  numeric;
  v_result jsonb;
BEGIN
  SELECT score INTO v_score FROM agents.evaluation_runs WHERE id = p_run_id;

  INSERT INTO agents.evaluation_baselines (evaluation_id, run_id, score)
  VALUES (p_evaluation_id, p_run_id, v_score)
  ON CONFLICT (evaluation_id)
  DO UPDATE SET run_id = EXCLUDED.run_id, score = EXCLUDED.score
  RETURNING to_jsonb(agents.evaluation_baselines.*) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, runner_paused)
  VALUES (p_ai_lenser_id, p_paused)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET runner_paused = p_paused,
        updated_at    = now();

  -- Mirror legacy policy_evaluations row when pausing, matching fn_pause_agent shape.
  IF p_paused THEN
    INSERT INTO agents.policy_evaluations (
      ai_lenser_id,
      evaluation_point,
      policy_type,
      verdict,
      reason,
      context,
      evaluated_at
    ) VALUES (
      p_ai_lenser_id,
      'pre_run',
      'pause',
      'deny',
      'runner paused by owner',
      jsonb_build_object('paused_by', auth.uid(), 'source', 'fn_set_runner_paused'),
      now()
    );
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) IS 'Canonical pause/resume RPC. Prefer over fn_pause_agent / fn_resume_agent.';



CREATE OR REPLACE FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    calendar_id = p_calendar_id
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") IS 'Security wrapper: assign a calendar to a workflow schedule owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    pre_dispatch_condition = p_condition
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") IS 'Security wrapper: update pre_dispatch_condition on a workflow schedule.';



CREATE OR REPLACE FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    inputs_rotation = p_rotation
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") IS 'Security wrapper: replace inputs_rotation on a workflow schedule.';



CREATE OR REPLACE FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_catalog'
    AS $$
BEGIN
  IF p_secret IS NULL OR length(p_secret) < 16 THEN
    RAISE EXCEPTION 'webhook signing secret must be at least 16 chars'
      USING ERRCODE = '22023';
  END IF;
  -- Persist database-wide so subsequent sessions see it.
  EXECUTE format('ALTER DATABASE %I SET app.webhook_signing_secret = %L',
                 current_database(), p_secret);
END;
$$;


ALTER FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") IS 'Phase P. Persists app.webhook_signing_secret as a database-wide GUC. service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'pg_catalog'
    AS $$
BEGIN
  EXECUTE format('ALTER DATABASE %I SET app.webhook_strict_signing = %L',
                 current_database(),
                 CASE WHEN p_strict THEN 'true' ELSE 'false' END);
END;
$$;


ALTER FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") RETURNS boolean
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  -- Self-notification check
  IF p_recipient_id = p_actor_id THEN
    RETURN false;
  END IF;

  -- Block check
  IF public.fn_is_notification_blocked(p_actor_id, p_recipient_id) THEN
    RETURN false;
  END IF;

  -- Mute check
  IF public.fn_is_notification_muted(p_recipient_id, p_type) THEN
    RETURN false;
  END IF;

  RETURN true;
EXCEPTION WHEN OTHERS THEN
  RAISE WARNING 'fn_should_send_notification failed: %', SQLERRM;
  RETURN true;  -- fail-open — never suppress by accident
END;
$$;


ALTER FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  v_tournament    battles.tournaments;
  v_contenders    battles.tournament_contenders[];
  v_round         battles.tournament_rounds;
  i               INT;
  v_a             battles.tournament_contenders;
  v_b             battles.tournament_contenders;
  v_battle        battles.battles;
  v_match         battles.tournament_matches;
BEGIN
  SELECT * INTO v_tournament FROM battles.tournaments WHERE id = p_tournament_id;

  IF v_tournament.status <> 'registration' THEN
    RAISE EXCEPTION 'Tournament must be in registration status to start';
  END IF;

  -- Collect contenders (assign seeds by registration order)
  SELECT array_agg(tc ORDER BY tc.created_at)
  INTO   v_contenders
  FROM   battles.tournament_contenders tc
  WHERE  tc.tournament_id = p_tournament_id;

  IF array_length(v_contenders, 1) < 2 THEN
    RAISE EXCEPTION 'Need at least 2 contenders to start';
  END IF;

  -- Update seeds
  FOR i IN 1..array_length(v_contenders, 1) LOOP
    UPDATE battles.tournament_contenders
    SET seed = i, status = 'active'
    WHERE id = v_contenders[i].id;
  END LOOP;

  -- Create round 1
  INSERT INTO battles.tournament_rounds (tournament_id, round_number, status)
  VALUES (p_tournament_id, 1, 'active')
  RETURNING * INTO v_round;

  -- Pair contenders (1 vs N, 2 vs N-1, …) — standard bracket seeding
  i := 1;
  WHILE i <= array_length(v_contenders, 1) / 2 LOOP
    v_a := v_contenders[i];
    v_b := v_contenders[array_length(v_contenders, 1) - i + 1];

    -- Create an auto-starting battle for this match
    INSERT INTO battles.battles (
      title, task_prompt, battle_type, voter_eligibility, status, slug,
      execution_starts_at, auto_publish, voting_duration_hours,
      ai_judge_enabled, ai_judge_model_key,
      creator_lenser_id
    )
    SELECT
      v_tournament.title || ' — Round 1 Match ' || i,
      'Tournament match between ' || v_a.lenser_id || ' and ' || v_b.lenser_id,
      v_tournament.battle_type,
      'open',
      'open',
      lower(regexp_replace(v_tournament.title, '[^a-zA-Z0-9]', '-', 'g')) || '-r1m' || i,
      now() + INTERVAL '5 minutes',
      TRUE,
      24,
      v_tournament.ai_judge_enabled,
      CASE WHEN v_tournament.ai_judge_enabled THEN 'claude-sonnet-4-6' ELSE NULL END,
      v_tournament.creator_lenser_id
    RETURNING * INTO v_battle;

    -- Create match record
    INSERT INTO battles.tournament_matches
      (tournament_id, round_id, battle_id, contender_a_id, contender_b_id)
    VALUES
      (p_tournament_id, v_round.id, v_battle.id, v_a.id, v_b.id)
    RETURNING * INTO v_match;

    i := i + 1;
  END LOOP;

  -- Update tournament status
  UPDATE battles.tournaments
  SET status = 'active', updated_at = now()
  WHERE id = p_tournament_id;
END;
$$;


ALTER FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb", "p_global_model_id" "text" DEFAULT NULL::"text", "p_idempotency_key" "text" DEFAULT NULL::"text", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
BEGIN
  RETURN lenses.fn_start_workflow_run(
    p_workflow_id,
    p_inputs,
    p_global_model_id,
    p_idempotency_key,
    p_version_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") IS 'Canonical workflow-start RPC (single overload). Accepts optional p_version_id to pin execution to a specific immutable version; NULL resolves to head_version_id. D2+D4+Z2: anon callers rejected; idempotency_key window capped at 24h.';



CREATE OR REPLACE FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text" DEFAULT NULL::"text", "p_raw_key" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
BEGIN
    RETURN ai.fn_store_api_key(
        p_provider => p_provider,
        p_label    => p_label,
        p_raw_key  => p_raw_key
    );
END;
$$;


ALTER FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") IS 'Public REST wrapper for ai.fn_store_api_key. Encrypts a BYOK API key via Vault and stores a reference row in ai.keys. The ai schema is not exposed in PostgREST — this wrapper is the only REST-callable entry point for key storage. Returns the key row UUID. Added in migration 000057.';



CREATE OR REPLACE FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") RETURNS TABLE("id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "content_text" "text", "content_url" "text", "status" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  INSERT INTO battles.submissions (battle_id, contender_id, content_text, content_url, status)
  VALUES (p_battle_id, p_contender_id, p_content_text, NULL, 'submitted')
  RETURNING id, battle_id, contender_id, content_text, content_url, status;
$$;


ALTER FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean DEFAULT false, "p_rationale" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
  v_vote_id   uuid;
BEGIN
  -- Guard: battle must be in voting phase
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles
    WHERE id = p_battle_id AND status = 'voting'
  ) THEN
    RAISE EXCEPTION 'battle_not_in_voting_phase: %', p_battle_id;
  END IF;

  -- Guard: contender must belong to the battle
  IF NOT EXISTS (
    SELECT 1 FROM battles.contenders
    WHERE id = p_voted_contender_id AND battle_id = p_battle_id
  ) THEN
    RAISE EXCEPTION 'contender_not_in_battle: %', p_voted_contender_id;
  END IF;

  -- Insert vote (uq_votes_battle_voter unique constraint prevents double-vote)
  INSERT INTO battles.votes (
    battle_id, voter_lenser_id, vote_value,
    voted_contender_id, is_draw, rationale
  )
  VALUES (
    p_battle_id, v_lenser_id, p_vote_value,
    p_voted_contender_id, p_is_draw, p_rationale
  )
  RETURNING id INTO v_vote_id;

  -- Upsert vote aggregates (atomic counter update)
  INSERT INTO battles.vote_aggregates (
    battle_id, contender_id,
    raw_vote_count, weighted_vote_sum, draw_count
  )
  VALUES (
    p_battle_id, p_voted_contender_id,
    1, 1, CASE WHEN p_is_draw THEN 1 ELSE 0 END
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE SET
    raw_vote_count    = battles.vote_aggregates.raw_vote_count + 1,
    weighted_vote_sum = battles.vote_aggregates.weighted_vote_sum + 1,
    draw_count        = battles.vote_aggregates.draw_count +
                        CASE WHEN p_is_draw THEN 1 ELSE 0 END,
    updated_at        = now();

  -- Increment denormalized total on the battle row
  UPDATE battles.battles
  SET total_vote_count = total_vote_count + 1
  WHERE id = p_battle_id;

  -- TODO Phase C: award XP via xp rule lookup once rule_id for 'BATTLE_VOTED' is known.
  -- INSERT INTO xp.events (lenser_id, app_id, rule_id, action_key, xp, base_xp, source,
  --                        source_ref_type, source_ref_id)
  -- VALUES (v_lenser_id, <app_id>, <rule_id>, 'BATTLE_VOTED', <xp>, <base_xp>, 'votes',
  --         'battle', p_battle_id);

  RETURN jsonb_build_object(
    'vote_id',   v_vote_id,
    'status',    'success',
    'battle_id', p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") IS 'Canonical vote submission path. Atomically: 1) inserts into battles.votes (uq_votes_battle_voter prevents double-vote), 2) upserts battles.vote_aggregates, 3) increments battles.total_vote_count. Phase A fix: removed broken xp.events INSERT (wrong columns). XP award deferred to Phase C once xp rule_id for BATTLE_VOTED is known. Direct INSERT on battles.votes is restricted to service_role (authenticated policy dropped in A2).';



CREATE OR REPLACE FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean DEFAULT false, "p_rationale" "text" DEFAULT NULL::"text") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
DECLARE
  v_voter_lenser_id  uuid;
  v_vote_id   uuid;
BEGIN
  v_voter_lenser_id := lensers.get_auth_lenser_id();

  -- Rate-limit: one vote per voter per battle per 60 s
  IF NOT battles.fn_check_vote_rate_limit(v_voter_lenser_id, p_battle_id) THEN
    RAISE EXCEPTION 'RATE_LIMIT: vote rate limit exceeded for battle %', p_battle_id
      USING ERRCODE = 'P0429';
  END IF;

  INSERT INTO battles.votes (
    battle_id,
    voter_lenser_id,
    voted_contender_id,
    vote_value,
    is_draw,
    rationale
  )
  VALUES (
    p_battle_id,
    v_voter_lenser_id,
    p_voted_contender_id,
    p_vote_value,
    p_is_draw,
    p_rationale
  )
  RETURNING id INTO v_vote_id;

  RETURN jsonb_build_object(
    'vote_id',   v_vote_id,
    'status',    'submitted',
    'battle_id', p_battle_id
  );
END;
$$;


ALTER FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_profile     agents.memory_profiles;
  v_count       integer;
  v_last_at     timestamptz;
  v_scope_stats jsonb;
BEGIN
  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  SELECT COUNT(*), MAX(created_at)
    INTO v_count, v_last_at
    FROM agents.memories
   WHERE profile_id = p_profile_id AND is_redacted = false;

  SELECT COALESCE(jsonb_object_agg(scope, c), '{}'::jsonb)
    INTO v_scope_stats
    FROM (
      SELECT scope, COUNT(*) AS c
        FROM agents.memories
       WHERE profile_id = p_profile_id AND is_redacted = false
       GROUP BY scope
    ) s;

  RETURN jsonb_build_object(
    'profile_id',      p_profile_id,
    'count',           COALESCE(v_count, 0),
    'last_written_at', v_last_at,
    'scopes',          v_scope_stats
  );
END;
$$;


ALTER FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") IS 'Stub summary returning counts and last write. Replaceable by an LLM/embedding-backed summarizer behind the same signature (Open-Closed).';



CREATE OR REPLACE FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'agents', 'auth'
    AS $$
DECLARE
  v_human_id          uuid;
  v_current_active_id uuid;
  v_from_ai_lenser_id uuid;
  v_target_profile_id uuid;
  v_to_ai_lenser_id   uuid;
BEGIN
  SELECT p.id INTO v_human_id
  FROM lensers.profiles p
  WHERE p.user_id = auth.uid()
    AND p.type    = 'human'
    AND p.status  = 'active'
  LIMIT 1;

  IF v_human_id IS NULL THEN
    RAISE EXCEPTION 'No active human profile found for this user';
  END IF;

  SELECT pref.active_lenser_id INTO v_current_active_id
  FROM lensers.preferences pref
  WHERE pref.lenser_id = v_human_id
  LIMIT 1;

  SELECT al.id INTO v_from_ai_lenser_id
  FROM agents.ai_lensers al
  JOIN lensers.profiles ap ON ap.id = al.profile_id
  WHERE ap.id = v_current_active_id
  LIMIT 1;

  IF p_lenser_id = v_human_id THEN
    UPDATE lensers.preferences
    SET active_lenser_id = NULL,
        updated_at       = now()
    WHERE lenser_id = v_human_id;

    IF v_from_ai_lenser_id IS NOT NULL THEN
      INSERT INTO agents.workspace_switches (human_lenser_id, from_ai_lenser_id, to_ai_lenser_id)
      VALUES (v_human_id, v_from_ai_lenser_id, NULL);
    END IF;

    RETURN;
  END IF;

  SELECT ai_p.id, al.id
  INTO v_target_profile_id, v_to_ai_lenser_id
  FROM agents.ownerships o
  JOIN agents.ai_lensers al ON al.id = o.ai_lenser_id
  JOIN lensers.profiles ai_p ON ai_p.id = al.profile_id
  WHERE o.owner_lenser_id = v_human_id
    AND (ai_p.id = p_lenser_id OR al.id = p_lenser_id)
    AND o.role = 'owner'
    AND o.revoked_at IS NULL
    AND ai_p.status = 'active'
  LIMIT 1;

  IF v_target_profile_id IS NULL THEN
    RAISE EXCEPTION 'Cannot switch: profile not found or not owned'
      USING ERRCODE = '42501';
  END IF;

  UPDATE lensers.preferences
  SET active_lenser_id = v_target_profile_id,
      updated_at       = now()
  WHERE lenser_id = v_human_id;

  INSERT INTO agents.workspace_switches (human_lenser_id, from_ai_lenser_id, to_ai_lenser_id)
  VALUES (v_human_id, v_from_ai_lenser_id, v_to_ai_lenser_id);
END;
$$;


ALTER FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") IS 'Switches the authenticated user''s active workspace to the given lenser ID. Accepts the human profile id, an owned AI profile id, or that owned AI lenser runtime id. Stores AI selection in lensers.preferences.active_lenser_id as the profile id and appends agents.workspace_switches audit rows.';



CREATE OR REPLACE FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer DEFAULT 100) RETURNS TABLE("id" "uuid", "object_class" "text", "object_id" "text", "op" "text", "payload" "jsonb", "vclock" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_sync_pull(p_envelope, p_object_classes, p_limit);
$$;


ALTER FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") RETURNS TABLE("applied_count" integer, "rejected_count" integer, "rejections" "jsonb")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_sync_push(p_envelope);
$$;


ALTER FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") RETURNS TABLE("object_class" "text", "watermark" timestamp with time zone, "outbox_depth" bigint, "last_error" "text")
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'devices', 'extensions'
    AS $$
  SELECT * FROM devices.fn_sync_status(p_device_id);
$$;


ALTER FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'analytics'
    AS $$
BEGIN
  INSERT INTO analytics.tag_activity_events (tag_id, entity_type, entity_id, activity_type, actor_id)
  SELECT 
    (e->>'tag_id')::uuid,
    ((e->>'entity_type')::content.entity_type_enum),
    (e->>'entity_id')::uuid,
    (e->>'activity_type')::text,
    (e->>'actor_id')::uuid
  FROM jsonb_array_elements(p_events) AS e
  WHERE (e->>'tag_id') IS NOT NULL 
    AND (e->>'entity_id') IS NOT NULL;
END;
$$;


ALTER FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text" DEFAULT 'info'::"text", "p_node_id" "uuid" DEFAULT NULL::"uuid", "p_metadata" "jsonb" DEFAULT NULL::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_caller_id uuid := lensers.get_auth_lenser_id();
  v_tag_id    uuid;
BEGIN
  IF v_caller_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required' USING ERRCODE = 'insufficient_privilege';
  END IF;

  IF p_severity NOT IN ('info', 'warn', 'critical') THEN
    RAISE EXCEPTION 'invalid severity: %', p_severity USING ERRCODE = 'check_violation';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    WHERE r.id = p_run_id
      AND r.triggered_by = v_caller_id
  ) THEN
    RAISE EXCEPTION 'workflow_run % not found or not owned by caller', p_run_id
      USING ERRCODE = 'insufficient_privilege';
  END IF;

  INSERT INTO lenses.workflow_run_tags (run_id, tag, severity, node_id, metadata)
  VALUES (p_run_id, p_tag, p_severity, p_node_id, p_metadata)
  RETURNING id INTO v_tag_id;

  RETURN v_tag_id;
END;
$$;


ALTER FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") IS 'Classifies a workflow run with a tag (info|warn|critical). Owner-gated. Called by the browser executor + CF Worker to mark moderation_blocked / contract_violated / idempotent_replay / partial_success / high_token_usage runs. See docs/reference/workflows/execution-engine.md §Observability.';



CREATE OR REPLACE FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer DEFAULT 50) RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "visibility" "text", "created_at" timestamp with time zone, "created_count" bigint, "viewed_count" bigint, "reacted_count" bigint, "total_usage" bigint, "trend_score_7d" numeric)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'analytics'
    AS $$
  SELECT
    id,
    slug,
    name,
    visibility,
    created_at,
    created_count,
    viewed_count,
    reacted_count,
    total_usage,
    trend_score_7d
  FROM vw_tags_public_stats
  ORDER BY trend_score_7d DESC NULLS LAST, total_usage DESC NULLS LAST
  LIMIT LEAST(p_limit, 100);
$$;


ALTER FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text" DEFAULT 'en'::"text", "p_limit" integer DEFAULT 5) RETURNS TABLE("id" "uuid", "name" "text", "slug" "text", "visibility" "text", "total_usage" bigint, "lang_match" boolean)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
  SELECT
    t.id,
    COALESCE(tr_lang.name, tr_en.name, t.slug) AS name,
    t.slug,
    t.visibility::text,
    COALESCE(s.total_usage, 0)                  AS total_usage,
    (tr_lang.tag_id IS NOT NULL)                AS lang_match
  FROM content.tags t
  LEFT JOIN content.tag_translations tr_lang
    ON tr_lang.tag_id = t.id AND tr_lang.language_code = p_lang
  LEFT JOIN content.tag_translations tr_en
    ON tr_en.tag_id = t.id AND tr_en.language_code = 'en'
  LEFT JOIN vw_tags_public_stats s
    ON s.id = t.id
  WHERE
    t.visibility = 'public'
    AND (
      COALESCE(tr_lang.name, tr_en.name, t.slug) ILIKE '%' || p_query || '%'
      OR t.slug ILIKE '%' || p_query || '%'
    )
  ORDER BY
    lang_match DESC,
    CASE
      WHEN LOWER(COALESCE(tr_lang.name, tr_en.name, t.slug)) = LOWER(p_query) THEN 0
      ELSE 1
    END,
    COALESCE(s.total_usage, 0) DESC
  LIMIT p_limit;
$$;


ALTER FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_auto_start_delay_hours" integer DEFAULT 1) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
  UPDATE battles.templates
  SET recurrence_rule = p_recurrence_rule,
      next_run_at     = COALESCE(p_next_run_at, now())
  WHERE id = p_template_id
    AND creator_lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer DEFAULT 4) RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_threshold INTERVAL;
  v_count     INT := 0;
  v_run_id    UUID;
BEGIN
  v_threshold := make_interval(hours => p_stale_threshold_hours);

  FOR v_run_id IN
    SELECT id
    FROM   lenses.workflow_runs
    WHERE  status IN ('running', 'streaming', 'recovered')
      AND  (
             -- No heartbeat and started too long ago
             (heartbeat_at IS NULL     AND started_at  < now() - v_threshold)
             OR
             -- Heartbeat present but stale
             (heartbeat_at IS NOT NULL AND heartbeat_at < now() - v_threshold)
           )
    FOR UPDATE SKIP LOCKED
  LOOP
    -- Mark run terminal
    UPDATE lenses.workflow_runs
    SET    status       = 'timed_out',
           completed_at = COALESCE(completed_at, now())
    WHERE  id = v_run_id;

    -- Cascade: mark all non-terminal node results as timed_out
    UPDATE lenses.workflow_node_results
    SET    status        = 'timed_out',
           completed_at  = COALESCE(completed_at, now()),
           error_message = COALESCE(error_message, 'run_timed_out_by_sweeper')
    WHERE  run_id = v_run_id
      AND  status NOT IN (
             'completed', 'failed', 'cancelled',
             'timed_out', 'skipped', 'blocked', 'invalidated'
           );

    v_count := v_count + 1;
  END LOOP;

  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) IS 'Sweeper: marks workflow runs that have been in running/streaming/recovered state with no heartbeat for > p_stale_threshold_hours (default 4h) as timed_out, cascading to node results. Uses FOR UPDATE SKIP LOCKED to avoid contention with live workers. Intended to run every 5 minutes via pg_cron.';



CREATE OR REPLACE FUNCTION "public"."fn_timeout_stale_runs_safe"() RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
  SELECT automation.fn_run_with_lock(
    'timeout-stale-runs',
    'SELECT public.fn_timeout_stale_runs(4)'
  );
$$;


ALTER FUNCTION "public"."fn_timeout_stale_runs_safe"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_timeout_stale_runs_safe"() IS 'pg_cron entry-point for fn_timeout_stale_runs. Wraps execution in automation.fn_run_with_lock (advisory lock + 55s statement_timeout) to prevent overlapping ticks.';



CREATE OR REPLACE FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "name" "text", "match_event_type" "text", "match_filter" "jsonb", "action_kind" "text", "action_config" "jsonb", "is_active" boolean, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'automation'
    AS $$
  UPDATE automation.trigger_rules
  SET    is_active  = p_is_active,
         updated_at = now()
  WHERE  id        = p_rule_id
    AND  lenser_id = auth.uid()
  RETURNING
    id, lenser_id, name, match_event_type, match_filter,
    action_kind, action_config, is_active, created_at, updated_at;
$$;


ALTER FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) IS 'Security wrapper: set is_active on a trigger rule owned by the current user. Returns the updated row. No-ops silently if the rule does not belong to the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  UPDATE battles.templates
  SET is_public  = p_is_public,
      updated_at = now()
  WHERE id = p_template_id;
$$;


ALTER FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id, global_kill_switch)
  VALUES (p_ai_lenser_id, p_enabled)
  ON CONFLICT (ai_lenser_id) DO UPDATE
    SET global_kill_switch = p_enabled,
        updated_at         = now();

  IF p_enabled THEN
    INSERT INTO agents.policy_evaluations (
      ai_lenser_id,
      evaluation_point,
      policy_type,
      verdict,
      reason,
      context,
      evaluated_at
    ) VALUES (
      p_ai_lenser_id,
      'pre_run',
      'kill_switch',
      'deny',
      'global kill switch activated',
      jsonb_build_object('activated_by', auth.uid()),
      now()
    );
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'audit', 'public'
    AS $$
DECLARE
  v_owner_id UUID;
BEGIN
  IF p_visibility NOT IN ('public', 'private', 'unlisted') THEN
    RAISE EXCEPTION 'Invalid visibility value: %. Must be public, private, or unlisted', p_visibility
      USING ERRCODE = 'P0001';
  END IF;

  SELECT owner_lenser_id INTO v_owner_id
  FROM media.objects
  WHERE id = p_object_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'media_object_not_found: %', p_object_id USING ERRCODE = 'P0001';
  END IF;

  IF v_owner_id != auth.uid() THEN
    RAISE EXCEPTION 'media_visibility_forbidden: caller does not own object %', p_object_id
      USING ERRCODE = 'P0001';
  END IF;

  UPDATE media.objects
  SET visibility = p_visibility
  WHERE id = p_object_id;

  -- Audit row written by trg_media_audit_visibility trigger (see below)
END;
$$;


ALTER FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") IS 'AT: Toggles visibility of a media object. Owner only. The audit trigger writes audit.events. SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
  UPDATE lenses.workflow_schedules
  SET    is_active  = p_is_active
  WHERE  id        = p_schedule_id
    AND  workflow_id IN (
      SELECT w.id FROM lenses.workflows w
      WHERE w.lenser_id = lensers.get_auth_lenser_id()
    );
$$;


ALTER FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) IS 'Security wrapper: toggle is_active on a workflow schedule owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'media', 'public'
    AS $$
BEGIN
  UPDATE media.objects
  SET owner_lenser_id = p_new_owner_id
  WHERE id = p_object_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'media_object_not_found: %', p_object_id USING ERRCODE = 'P0001';
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") IS 'AT: Transfers media object ownership. Service_role only (used on agent deletion). SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_trg_battles_status_webhook"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  IF NEW.status IS DISTINCT FROM OLD.status THEN
    PERFORM public.fn_battles_notify_webhooks(
      NEW.id,
      'status_change',
      row_to_json(NEW)::JSONB
    );
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."fn_trg_battles_status_webhook"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_trg_votes_velocity_check"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
BEGIN
  -- Skip rate-limit check for service_role and postgres (seed/admin inserts)
  IF auth.role() = 'authenticated' THEN
    PERFORM public.fn_check_vote_velocity(NEW.voter_lenser_id);
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."fn_trg_votes_velocity_check"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
DECLARE
  v_source_id uuid;
BEGIN
  SELECT id INTO v_source_id
  FROM lensers.profiles WHERE user_id = auth.uid();

  IF v_source_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  DELETE FROM lensers.relationships
  WHERE source_profile_id = v_source_id
    AND target_profile_id = p_target_profile_id
    AND status = 'blocked';

  RETURN jsonb_build_object('blocked', false);
END;
$$;


ALTER FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_caller_human_id uuid;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM agents.ownerships o
    WHERE o.ai_lenser_id = p_ai_lenser_id
      AND o.owner_lenser_id = v_caller_human_id
      AND o.role IN ('owner', 'co_owner')
      AND o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  UPDATE agents.ai_lensers
  SET personality_note = p_personality_note
  WHERE id = p_ai_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") IS 'Owner-only update of an AI lenser personality_note. Authorizes via the human lenser ownership record.';



CREATE OR REPLACE FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
BEGIN
  PERFORM agents.fn_update_agent_policy(p_ai_lenser_id, p_patch);
END;
$$;


ALTER FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'agents', 'auth'
    AS $$
DECLARE
  v_profile_id uuid;
BEGIN
  IF NOT EXISTS (
    SELECT 1
    FROM   agents.ownerships o
    WHERE  o.ai_lenser_id   = p_ai_lenser_id
      AND  o.owner_lenser_id = lensers.get_auth_human_lenser_id()
      AND  o.role            = 'owner'
      AND  o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  SELECT al.profile_id INTO v_profile_id
  FROM   agents.ai_lensers al
  WHERE  al.id = p_ai_lenser_id;

  IF v_profile_id IS NULL THEN
    RAISE EXCEPTION 'AI lenser not found'
      USING ERRCODE = 'P0002';
  END IF;

  UPDATE lensers.profiles
  SET
    display_name = CASE WHEN p_patch ? 'display_name' THEN p_patch->>'display_name' ELSE display_name END,
    avatar_url   = CASE WHEN p_patch ? 'avatar_url'   THEN p_patch->>'avatar_url'   ELSE avatar_url   END,
    banner_url   = CASE WHEN p_patch ? 'banner_url'   THEN p_patch->>'banner_url'   ELSE banner_url   END,
    bio          = CASE WHEN p_patch ? 'bio'          THEN p_patch->>'bio'          ELSE bio          END,
    headline     = CASE WHEN p_patch ? 'headline'     THEN p_patch->>'headline'     ELSE headline     END,
    website_url  = CASE WHEN p_patch ? 'website_url'  THEN p_patch->>'website_url'  ELSE website_url  END,
    updated_at   = now()
  WHERE id   = v_profile_id
    AND type = 'ai';
END;
$$;


ALTER FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") IS 'Allows an owner lenser to patch display_name, avatar_url, banner_url, bio, headline, website_url of an AI lenser they own. Validates ownership via agents.ownerships.';



CREATE OR REPLACE FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text" DEFAULT NULL::"text", "p_task_prompt" "text" DEFAULT NULL::"text", "p_battle_type" "text" DEFAULT NULL::"text", "p_voter_eligibility" "text" DEFAULT NULL::"text", "p_handicap_config" "jsonb" DEFAULT NULL::"jsonb", "p_workflow_id" "uuid" DEFAULT NULL::"uuid", "p_lens_id" "uuid" DEFAULT NULL::"uuid", "p_forum_thread_id" "uuid" DEFAULT NULL::"uuid", "p_shared_input_snapshot" "jsonb" DEFAULT NULL::"jsonb", "p_lenser_battle_policy" "jsonb" DEFAULT NULL::"jsonb", "p_task_source" "text" DEFAULT NULL::"text", "p_contender_structure" "text" DEFAULT NULL::"text", "p_judging_mode" "text" DEFAULT NULL::"text", "p_challenge_type" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "task_prompt" "text", "status" "text", "total_vote_count" integer, "published_at" timestamp with time zone, "voting_opens_at" timestamp with time zone, "voting_closes_at" timestamp with time zone, "battle_type" "text", "voter_eligibility" "text", "handicap_config" "jsonb", "creator_lenser_id" "uuid", "forum_thread_id" "text", "workflow_id" "uuid", "lens_id" "uuid", "execution_starts_at" timestamp with time zone, "auto_publish" boolean, "voting_duration_hours" integer, "vote_velocity" numeric, "og_image_url" "text", "winner_contender_id" "uuid", "parent_battle_id" "uuid", "deleted_at" timestamp with time zone, "task_source" "text", "contender_structure" "text", "judging_mode" "text", "challenge_type" "text", "shared_input_snapshot" "jsonb", "lenser_battle_policy" "jsonb")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  UPDATE battles.battles b
  SET
    title                 = COALESCE(p_title,                                              b.title),
    task_prompt           = COALESCE(p_task_prompt,                                        b.task_prompt),
    battle_type           = COALESCE(p_battle_type::battles.battle_type_enum,              b.battle_type),
    voter_eligibility     = COALESCE(p_voter_eligibility::battles.voter_eligibility_enum,  b.voter_eligibility),
    handicap_config       = COALESCE(p_handicap_config,                                    b.handicap_config),
    workflow_id           = COALESCE(p_workflow_id,                                        b.workflow_id),
    lens_id               = COALESCE(p_lens_id,                                            b.lens_id),
    forum_thread_id       = COALESCE(p_forum_thread_id,                                    b.forum_thread_id),
    shared_input_snapshot = COALESCE(p_shared_input_snapshot,                              b.shared_input_snapshot),
    lenser_battle_policy  = COALESCE(p_lenser_battle_policy,                               b.lenser_battle_policy),
    task_source           = COALESCE(p_task_source,                                        b.task_source),
    contender_structure   = COALESCE(p_contender_structure::battles.contender_structure_enum, b.contender_structure),
    judging_mode          = COALESCE(p_judging_mode::battles.judging_mode_enum,            b.judging_mode),
    challenge_type        = COALESCE(p_challenge_type,                                     b.challenge_type)
  WHERE b.id = p_battle_id
    AND b.creator_lenser_id = v_lenser_id
  RETURNING
    b.id, b.slug, b.title, b.task_prompt, b.status::text, b.total_vote_count,
    b.published_at, b.voting_opens_at, b.voting_closes_at,
    b.battle_type::text, b.voter_eligibility::text, b.handicap_config,
    b.creator_lenser_id, b.forum_thread_id::text, b.workflow_id, b.lens_id,
    b.execution_starts_at, b.auto_publish, b.voting_duration_hours,
    b.vote_velocity, b.og_image_url, b.winner_contender_id,
    b.parent_battle_id, b.deleted_at,
    b.task_source, b.contender_structure::text, b.judging_mode::text,
    b.challenge_type, b.shared_input_snapshot, b.lenser_battle_policy;
END;
$$;


ALTER FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") IS 'Update a battle draft. Caller must be the creator. All params are optional — only non-null values overwrite. Now accepts and returns V2 concept-separation axes: task_source, contender_structure, judging_mode, challenge_type.';



CREATE OR REPLACE FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_lenser_id uuid := lensers.get_auth_lenser_id();
BEGIN
  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required.' USING ERRCODE = '42501';
  END IF;

  IF p_execution_starts_at IS NULL THEN
    RAISE EXCEPTION 'execution_starts_at must not be null.' USING ERRCODE = '22000';
  END IF;

  IF p_execution_starts_at <= now() THEN
    RAISE EXCEPTION 'execution_starts_at must be in the future (got %).',
      p_execution_starts_at USING ERRCODE = '22000';
  END IF;

  UPDATE battles.battles
  SET    execution_starts_at   = p_execution_starts_at,
         voting_duration_hours = p_voting_duration_hours,
         auto_publish          = p_auto_publish
  WHERE  id = p_battle_id
    AND  creator_lenser_id = v_lenser_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) IS 'Security wrapper: update execution scheduling fields on a battle owned by the current user. Rejects past execution timestamps server-side (issue #208).';



CREATE OR REPLACE FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_auth_lenser_id uuid;
  v_owner_lenser_id uuid;
BEGIN
  v_auth_lenser_id := lensers.get_auth_lenser_id();

  -- Resolve the team owner
  SELECT al.id INTO v_owner_lenser_id
  FROM agents.teams t
  JOIN agents.ai_lensers al ON al.id = t.ai_lenser_id
  WHERE t.id = p_team_id;

  IF v_owner_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Team % not found', p_team_id
      USING ERRCODE = 'P0002';
  END IF;

  -- Only the ai_lenser whose profile_id is the auth user may update roles
  IF NOT EXISTS (
    SELECT 1 FROM agents.ai_lensers al
    WHERE al.id = v_owner_lenser_id
      AND al.profile_id = v_auth_lenser_id
  ) THEN
    RAISE EXCEPTION 'Caller is not the owner of team %', p_team_id
      USING ERRCODE = '42501';
  END IF;

  -- Validate the target member belongs to the team
  IF NOT EXISTS (
    SELECT 1 FROM agents.team_members
    WHERE id = p_member_id AND team_id = p_team_id
  ) THEN
    RAISE EXCEPTION 'Member % not found in team %', p_member_id, p_team_id
      USING ERRCODE = 'P0002';
  END IF;

  UPDATE agents.team_members
  SET role = p_role, updated_at = now()
  WHERE id = p_member_id AND team_id = p_team_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") IS 'Updates a team member role. Caller must own the team. Only the role field is mutated — other member fields require direct RLS access.';



CREATE OR REPLACE FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT public.fn_worker_update_team_run(p_team_run_id, p_status, p_completed_at);
$$;


ALTER FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM content.threads
    WHERE id = p_thread_id AND lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RETURN;
  END IF;

  UPDATE content.entity_translations
  SET    title      = p_title,
         content    = p_content,
         updated_at = now()
  WHERE  entity_type::text = 'thread'
    AND  entity_id   = p_thread_id
    AND  is_original = true;
END;
$$;


ALTER FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") IS 'Security wrapper: update title and content of the original translation for a thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'lensers'
    AS $$
  UPDATE content.threads
  SET    visibility = p_visibility::content.visibility_enum,
         updated_at = now()
  WHERE  id        = p_thread_id
    AND  lenser_id = lensers.get_auth_lenser_id();
$$;


ALTER FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") IS 'Security wrapper: update visibility on a thread owned by the current user.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_visibility" "text" DEFAULT 'public'::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
declare
  v_lenser_id uuid;
begin
  if auth.uid() is null then
    raise exception 'Not authenticated';
  end if;

  v_lenser_id := lensers.get_auth_lenser_id();

  if v_lenser_id is null then
    raise exception 'Profile not found';
  end if;

  if char_length(trim(p_title)) < 1 or char_length(trim(p_title)) > 200 then
    raise exception 'Title must be between 1 and 200 characters';
  end if;

  if p_visibility not in ('public', 'private', 'unlisted') then
    raise exception 'Invalid visibility value';
  end if;

  return query
  update lenses.workflows w
  set
    title       = trim(p_title),
    description = p_description,
    visibility  = p_visibility,
    updated_at  = now()
  where w.id        = p_workflow_id
    and w.lenser_id = v_lenser_id
  returning
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility,
    w.battle_count,
    w.created_at,
    w.updated_at;

  if not found then
    raise exception 'Workflow not found or not owned by caller';
  end if;
end;
$$;


ALTER FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") IS 'Update workflow title, description, and visibility. Ownership resolved via lensers.get_auth_lenser_id() (supports AI lenser context switching). SECURITY DEFINER.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text", "p_retry_count" integer DEFAULT NULL::integer, "p_duration_ms" integer DEFAULT NULL::integer, "p_ttfb_ms" integer DEFAULT NULL::integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot update results for run %', p_run_id
      USING ERRCODE = '42501';
  END IF;

  UPDATE lenses.workflow_node_results
  SET
    status        = p_status,
    output_data   = COALESCE(p_output_data, output_data),
    error_message = COALESCE(p_error_message, error_message),
    retry_count   = COALESCE(p_retry_count, retry_count),
    -- Timing metrics are monotonic: once written, later partial updates
    -- (e.g. failed -> retrying -> completed) keep the most recent non-null
    -- value for duration, but ttfb only records the FIRST streamed chunk.
    duration_ms   = COALESCE(p_duration_ms, duration_ms),
    ttfb_ms       = COALESCE(ttfb_ms, p_ttfb_ms),
    started_at    = CASE
                      WHEN p_status IN ('running', 'streaming', 'retrying') AND started_at IS NULL
                        THEN now()
                      ELSE started_at
                    END,
    completed_at  = CASE
                      WHEN p_status IN ('completed', 'failed', 'cancelled',
                                        'skipped', 'timed_out', 'blocked', 'invalidated')
                        THEN now()
                      ELSE completed_at
                    END
  WHERE run_id = p_run_id AND node_id = p_node_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) IS 'Phase 6 overload: persists retry_count / duration_ms / ttfb_ms alongside status + output. All observability args are nullable — callers that omit them preserve prior values.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text", "p_retry_count" integer DEFAULT NULL::integer, "p_duration_ms" integer DEFAULT NULL::integer, "p_ttfb_ms" integer DEFAULT NULL::integer, "p_waiting_reason" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot update results for run %', p_run_id
      USING ERRCODE = '42501';
  END IF;

  UPDATE lenses.workflow_node_results
  SET
    status        = p_status,
    output_data   = COALESCE(p_output_data, output_data),
    error_message = COALESCE(p_error_message, error_message),
    retry_count   = COALESCE(p_retry_count, retry_count),
    duration_ms   = COALESCE(p_duration_ms, duration_ms),
    ttfb_ms       = COALESCE(ttfb_ms, p_ttfb_ms),
    -- Waiting reason: explicit overrides existing; null clears once the node
    -- transitions out of a waiting status.
    waiting_reason = CASE
                       WHEN p_status IN ('awaiting_dependency', 'queued', 'retrying')
                         THEN COALESCE(p_waiting_reason, waiting_reason)
                       ELSE NULL
                     END,
    started_at    = CASE
                      WHEN p_status IN ('running', 'streaming', 'retrying') AND started_at IS NULL
                        THEN now()
                      ELSE started_at
                    END,
    completed_at  = CASE
                      WHEN p_status IN ('completed', 'failed', 'cancelled',
                                        'skipped', 'timed_out', 'blocked', 'invalidated')
                        THEN now()
                      ELSE completed_at
                    END
  WHERE run_id = p_run_id AND node_id = p_node_id;

  -- Maintain run-level active node pointer (n8n-style "currently executing").
  -- Active when entering running/streaming/retrying, cleared on terminal.
  IF p_status IN ('running', 'streaming', 'retrying') THEN
    UPDATE lenses.workflow_runs
    SET active_node_id = p_node_id
    WHERE id = p_run_id;
  ELSIF p_status IN ('completed', 'failed', 'cancelled',
                     'skipped', 'timed_out', 'blocked', 'invalidated') THEN
    UPDATE lenses.workflow_runs
    SET active_node_id = NULL
    WHERE id = p_run_id
      AND active_node_id = p_node_id;
  END IF;
END;
$$;


ALTER FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") IS 'Canonical n8n-style node result update. Persists status + output + observability metrics, maintains waiting_reason for non-running statuses, and updates workflow_runs.active_node_id so the inspector can render the active step without scanning all node rows.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.workflow_runs r
    JOIN lenses.workflows w ON w.id = r.workflow_id
    WHERE r.id = p_run_id
      AND (w.lenser_id = v_caller_id OR w.visibility = 'public')
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller cannot update run %', p_run_id
      USING ERRCODE = '42501';
  END IF;

  UPDATE lenses.workflow_runs
  SET
    status       = p_status,
    started_at   = CASE
                     WHEN p_status IN ('running', 'streaming') AND started_at IS NULL
                       THEN now()
                     ELSE started_at
                   END,
    completed_at = CASE
                     WHEN p_status IN ('completed', 'failed', 'cancelled', 'timed_out')
                       THEN now()
                     ELSE completed_at
                   END,
    -- Clear worker ownership on terminal transitions so stale-claim queries
    -- do not revive a finished run.
    run_worker_id = CASE
                      WHEN p_status IN ('completed', 'failed', 'cancelled', 'timed_out')
                        THEN NULL
                      ELSE run_worker_id
                    END
  WHERE id = p_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") IS 'Updates a workflow_run status. Aligned with Phase 1 state machine — started_at written on first transition into running/streaming, completed_at for terminal states (completed, failed, cancelled, timed_out), run_worker_id cleared on terminals.';



CREATE OR REPLACE FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") RETURNS "agents"."workspace_settings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.workspace_settings;
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.workspace_settings (ai_lenser_id)
  VALUES (p_ai_lenser_id)
  ON CONFLICT (ai_lenser_id) DO NOTHING;

  UPDATE agents.workspace_settings
  SET default_model_id     = COALESCE((p_patch->>'default_model_id')::uuid,     default_model_id),
      default_provider_key = COALESCE(p_patch->>'default_provider_key',         default_provider_key),
      approval_default     = COALESCE(p_patch->>'approval_default',             approval_default),
      retention_days       = COALESCE((p_patch->>'retention_days')::integer,     retention_days),
      max_daily_credits    = COALESCE((p_patch->>'max_daily_credits')::integer,  max_daily_credits),
      webhooks             = COALESCE(p_patch->'webhooks',                       webhooks),
      api_access_enabled   = COALESCE((p_patch->>'api_access_enabled')::boolean, api_access_enabled),
      metadata             = COALESCE(p_patch->'metadata',                       metadata),
      max_parallel_runs    = COALESCE((p_patch->>'max_parallel_runs')::integer,  max_parallel_runs),
      global_kill_switch   = COALESCE((p_patch->>'global_kill_switch')::boolean, global_kill_switch),
      agent_paused         = COALESCE((p_patch->>'agent_paused')::boolean,       agent_paused),
      dark_launch_enabled  = COALESCE((p_patch->>'dark_launch_enabled')::boolean,dark_launch_enabled),
      dark_launch_pct      = COALESCE((p_patch->>'dark_launch_pct')::integer,    dark_launch_pct),
      budget_enforce       = COALESCE((p_patch->>'budget_enforce')::boolean,     budget_enforce),
      updated_at           = now()
  WHERE ai_lenser_id = p_ai_lenser_id
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid", "p_is_default" boolean DEFAULT true) RETURNS SETOF "agents"."lens_bindings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_caller_human_id uuid;
  v_profile_id uuid;
  v_binding_id uuid;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT al.profile_id INTO v_profile_id
  FROM agents.ai_lensers al
  JOIN agents.ownerships o ON o.ai_lenser_id = al.id
  WHERE al.id = p_ai_lenser_id
    AND o.owner_lenser_id = v_caller_human_id
    AND o.role IN ('owner', 'co_owner')
    AND o.revoked_at IS NULL
  LIMIT 1;

  IF v_profile_id IS NULL THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lenses.lenses l
    WHERE l.id = p_lens_id
      AND l.lenser_id = v_profile_id
  ) THEN
    RAISE EXCEPTION 'Main lens must be owned by the active AI workspace'
      USING ERRCODE = '42501';
  END IF;

  IF p_is_default THEN
    UPDATE agents.lens_bindings
    SET is_default = false
    WHERE ai_lenser_id = p_ai_lenser_id
      AND is_default = true;
  END IF;

  INSERT INTO agents.lens_bindings (ai_lenser_id, lens_id, version_id, is_default)
  VALUES (p_ai_lenser_id, p_lens_id, p_version_id, p_is_default)
  ON CONFLICT (ai_lenser_id, lens_id) DO UPDATE
    SET version_id = EXCLUDED.version_id,
        is_default = EXCLUDED.is_default
  RETURNING id INTO v_binding_id;

  INSERT INTO agents.action_logs (
    ai_lenser_id,
    action_type,
    result,
    metadata
  ) VALUES (
    p_ai_lenser_id,
    'binding_updated',
    'success',
    jsonb_build_object(
      'binding_kind', 'lens',
      'binding_id', v_binding_id,
      'lens_id', p_lens_id,
      'version_id', p_version_id,
      'is_default', p_is_default
    )
  );

  RETURN QUERY
  SELECT lb.*
  FROM agents.lens_bindings lb
  WHERE lb.id = v_binding_id;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid", "p_is_default" boolean DEFAULT true, "p_category_tags" "text"[] DEFAULT '{}'::"text"[]) RETURNS SETOF "agents"."lens_bindings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'lenses'
    AS $$
DECLARE
  v_caller_human_id uuid;
  v_profile_id      uuid;
  v_binding_id      uuid;
  v_is_personality  boolean;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  SELECT al.profile_id INTO v_profile_id
  FROM agents.ai_lensers al
  JOIN agents.ownerships o ON o.ai_lenser_id = al.id
  WHERE al.id = p_ai_lenser_id
    AND o.owner_lenser_id = v_caller_human_id
    AND o.role IN ('owner', 'co_owner')
    AND o.revoked_at IS NULL
  LIMIT 1;

  IF v_profile_id IS NULL THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  -- Accept lenses that are:
  --   a) owned by the AI workspace profile, OR
  --   b) owned by the calling human co-owner, OR
  --   c) public + published (platform/system lenses accessible to everyone)
  IF NOT EXISTS (
    SELECT 1 FROM lenses.lenses l
    WHERE l.id = p_lens_id
      AND (
        l.lenser_id = v_profile_id
        OR l.lenser_id = v_caller_human_id
        OR (l.visibility = 'public' AND l.status = 'published')
      )
  ) THEN
    RAISE EXCEPTION 'Lens must be owned by the AI workspace, its co-owner, or be a public published lens'
      USING ERRCODE = '42501';
  END IF;

  v_is_personality := 'personality' = ANY(p_category_tags);

  -- Clear the default flag only within the same category scope
  IF p_is_default THEN
    IF v_is_personality THEN
      UPDATE agents.lens_bindings
        SET is_default = false
        WHERE ai_lenser_id = p_ai_lenser_id
          AND is_default = true
          AND 'personality' = ANY(category_tags);
    ELSE
      UPDATE agents.lens_bindings
        SET is_default = false
        WHERE ai_lenser_id = p_ai_lenser_id
          AND is_default = true
          AND NOT ('personality' = ANY(category_tags));
    END IF;
  END IF;

  INSERT INTO agents.lens_bindings
    (ai_lenser_id, lens_id, version_id, is_default, category_tags)
  VALUES
    (p_ai_lenser_id, p_lens_id, p_version_id, p_is_default, p_category_tags)
  ON CONFLICT (ai_lenser_id, lens_id) DO UPDATE
    SET version_id    = EXCLUDED.version_id,
        is_default    = EXCLUDED.is_default,
        category_tags = EXCLUDED.category_tags
  RETURNING id INTO v_binding_id;

  INSERT INTO agents.action_logs (ai_lenser_id, action_type, result, metadata)
  VALUES (
    p_ai_lenser_id,
    'binding_updated',
    'success',
    jsonb_build_object(
      'binding_kind',  'lens',
      'binding_id',    v_binding_id,
      'lens_id',       p_lens_id,
      'version_id',    p_version_id,
      'is_default',    p_is_default,
      'category_tags', p_category_tags
    )
  );

  RETURN QUERY SELECT lb.* FROM agents.lens_bindings lb WHERE lb.id = v_binding_id LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean DEFAULT true) RETURNS SETOF "agents"."model_bindings"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers', 'ai'
    AS $$
DECLARE
  v_caller_human_id uuid;
  v_binding_id uuid;
BEGIN
  v_caller_human_id := lensers.get_auth_human_lenser_id();
  IF v_caller_human_id IS NULL THEN
    RAISE EXCEPTION 'Authentication required';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM agents.ownerships o
    WHERE o.ai_lenser_id = p_ai_lenser_id
      AND o.owner_lenser_id = v_caller_human_id
      AND o.role IN ('owner', 'co_owner')
      AND o.revoked_at IS NULL
  ) THEN
    RAISE EXCEPTION 'Forbidden: you do not own this AI lenser'
      USING ERRCODE = '42501';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM ai.models m
    WHERE m.id = p_model_id
      AND m.is_active = true
  ) THEN
    RAISE EXCEPTION 'Model not found or inactive'
      USING ERRCODE = 'P0001';
  END IF;

  IF p_is_default THEN
    UPDATE agents.model_bindings
    SET is_default = false
    WHERE ai_lenser_id = p_ai_lenser_id
      AND is_default = true;
  END IF;

  INSERT INTO agents.model_bindings (ai_lenser_id, model_id, is_default)
  VALUES (p_ai_lenser_id, p_model_id, p_is_default)
  ON CONFLICT (ai_lenser_id, model_id) DO UPDATE
    SET is_default = EXCLUDED.is_default
  RETURNING id INTO v_binding_id;

  INSERT INTO agents.action_logs (
    ai_lenser_id,
    action_type,
    result,
    metadata
  ) VALUES (
    p_ai_lenser_id,
    'binding_updated',
    'success',
    jsonb_build_object(
      'binding_kind', 'model',
      'binding_id', v_binding_id,
      'model_id', p_model_id,
      'is_default', p_is_default
    )
  );

  RETURN QUERY
  SELECT mb.*
  FROM agents.model_bindings mb
  WHERE mb.id = v_binding_id;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[] DEFAULT '{}'::"text"[]) RETURNS TABLE("id" "uuid", "ai_lenser_id" "uuid", "owner_lenser_id" "uuid", "role" "text", "permission_scope" "text"[], "granted_at" timestamp with time zone, "revoked_at" timestamp with time zone, "owner_handle" "text", "owner_display_name" "text", "owner_avatar_url" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $$
DECLARE
  v_caller_human_id uuid := lensers.get_auth_human_lenser_id();
BEGIN
  IF NOT agents.can_manage_ai_lenser(p_ai_lenser_id) THEN
    RAISE EXCEPTION 'Not authorized to manage this AI Lenser.'
      USING ERRCODE = '42501';
  END IF;

  IF p_role NOT IN ('co_owner', 'operator') THEN
    RAISE EXCEPTION 'Only co_owner and operator may be granted through this RPC.'
      USING ERRCODE = '22023';
  END IF;

  IF p_owner_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Delegate owner_lenser_id is required.'
      USING ERRCODE = '22023';
  END IF;

  IF p_owner_lenser_id = v_caller_human_id THEN
    RAISE EXCEPTION 'The primary owner already has access and cannot be delegated through this RPC.'
      USING ERRCODE = '22023';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM agents.ai_lensers al
    WHERE al.id = p_ai_lenser_id
  ) THEN
    RAISE EXCEPTION 'AI Lenser not found.'
      USING ERRCODE = 'P0002';
  END IF;

  IF NOT EXISTS (
    SELECT 1
    FROM lensers.profiles p
    WHERE p.id = p_owner_lenser_id
      AND p.type = 'human'
      AND p.status = 'active'
  ) THEN
    RAISE EXCEPTION 'Delegate must be an active human Lenser.'
      USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.ownerships (
    ai_lenser_id,
    owner_lenser_id,
    role,
    permission_scope,
    granted_at,
    revoked_at
  )
  VALUES (
    p_ai_lenser_id,
    p_owner_lenser_id,
    p_role,
    COALESCE(p_permission_scope, '{}'::text[]),
    now(),
    NULL
  )
  ON CONFLICT (ai_lenser_id, owner_lenser_id)
  DO UPDATE SET
    role = EXCLUDED.role,
    permission_scope = EXCLUDED.permission_scope,
    granted_at = now(),
    revoked_at = NULL;

  RETURN QUERY
  SELECT
    o.id,
    o.ai_lenser_id,
    o.owner_lenser_id,
    o.role,
    o.permission_scope,
    o.granted_at,
    o.revoked_at,
    p.handle,
    p.display_name,
    p.avatar_url
  FROM agents.ownerships o
  JOIN lensers.profiles p
    ON p.id = o.owner_lenser_id
  WHERE o.ai_lenser_id = p_ai_lenser_id
    AND o.owner_lenser_id = p_owner_lenser_id
  LIMIT 1;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) IS 'Creates or updates a co_owner/operator delegate for an AI Lenser. owner transfers stay out of scope for this RPC.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text" DEFAULT NULL::"text", "p_recent_output_summary" "text" DEFAULT NULL::"text", "p_blocker_summary" "text" DEFAULT NULL::"text", "p_started_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_existing_id uuid;
  v_result      jsonb;
BEGIN
  SELECT id INTO v_existing_id
  FROM agents.agent_run_steps
  WHERE team_run_id = p_team_run_id AND workflow_node_id = p_workflow_node_id
  LIMIT 1;

  IF v_existing_id IS NOT NULL THEN
    UPDATE agents.agent_run_steps
    SET status                = p_status,
        title                 = p_title,
        current_task          = COALESCE(p_current_task, current_task),
        recent_output_summary = COALESCE(p_recent_output_summary, recent_output_summary),
        blocker_summary       = COALESCE(p_blocker_summary, blocker_summary),
        completed_at          = COALESCE(p_completed_at, completed_at),
        updated_at            = now()
    WHERE id = v_existing_id
    RETURNING to_jsonb(agents.agent_run_steps.*) INTO v_result;
  ELSE
    INSERT INTO agents.agent_run_steps
      (team_run_id, workflow_node_id, lane, title, status,
       current_task, recent_output_summary, blocker_summary,
       started_at, completed_at)
    VALUES
      (p_team_run_id, p_workflow_node_id, p_lane, p_title, p_status,
       p_current_task, p_recent_output_summary, p_blocker_summary,
       COALESCE(p_started_at, now()), p_completed_at)
    RETURNING to_jsonb(agents.agent_run_steps.*) INTO v_result;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid" DEFAULT NULL::"uuid", "p_funding_source" "text" DEFAULT 'platform_credit'::"text", "p_byok_key_ref_id" "uuid" DEFAULT NULL::"uuid", "p_max_tokens" integer DEFAULT 4096, "p_temperature" numeric DEFAULT 0.70) RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'lensers'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  -- Ensure caller owns the battle
  IF NOT EXISTS (
    SELECT 1 FROM battles.battles b
    WHERE b.id = p_battle_id
      AND b.creator_lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'upsert_battle_execution_config_forbidden' USING ERRCODE = '42501';
  END IF;

  INSERT INTO battles.execution_configs
    (battle_id, contender_id, provider_key, model_key, model_id, funding_source,
     byok_key_ref_id, max_tokens, temperature)
  VALUES
    (p_battle_id, p_contender_id, p_provider_key, p_model_key, p_model_id,
     p_funding_source, p_byok_key_ref_id, p_max_tokens, p_temperature)
  ON CONFLICT (battle_id, contender_id)
  DO UPDATE SET
    provider_key   = EXCLUDED.provider_key,
    model_key      = EXCLUDED.model_key,
    model_id       = EXCLUDED.model_id,
    funding_source = EXCLUDED.funding_source,
    byok_key_ref_id = EXCLUDED.byok_key_ref_id,
    max_tokens     = EXCLUDED.max_tokens,
    temperature    = EXCLUDED.temperature,
    updated_at     = now()
  RETURNING to_jsonb(battles.execution_configs.*) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers'
    AS $$
BEGIN
  INSERT INTO public.notification_preferences (lenser_id, notification_type, enabled)
  VALUES (lensers.get_auth_lenser_id(), p_type, p_enabled)
  ON CONFLICT (lenser_id, notification_type)
  DO UPDATE SET enabled = p_enabled, updated_at = now();
END;
$$;


ALTER FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text" DEFAULT NULL::"text", "p_status" "text" DEFAULT 'unconfigured'::"text", "p_ai_key_id" "uuid" DEFAULT NULL::"uuid") RETURNS "agents"."provider_configs"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_row agents.provider_configs;
BEGIN
  IF p_status NOT IN ('healthy', 'error', 'unconfigured') THEN
    RAISE EXCEPTION 'invalid status value: %', p_status USING ERRCODE = '22023';
  END IF;

  INSERT INTO agents.provider_configs (
    ai_lenser_id,
    provider_key,
    ai_key_id,
    base_url,
    status,
    configured_at,
    last_checked_at,
    updated_at
  )
  VALUES (
    p_ai_lenser_id,
    p_provider_key,
    p_ai_key_id,
    p_base_url,
    p_status,
    -- configured_at: set on first key association; preserved on subsequent updates
    CASE WHEN p_ai_key_id IS NOT NULL THEN now() ELSE NULL END,
    -- last_checked_at: set whenever a health check result (healthy/error) is recorded
    CASE WHEN p_status IN ('healthy', 'error') THEN now() ELSE NULL END,
    now()
  )
  ON CONFLICT (ai_lenser_id, provider_key) DO UPDATE
    SET ai_key_id       = COALESCE(EXCLUDED.ai_key_id,   provider_configs.ai_key_id),
        base_url        = COALESCE(EXCLUDED.base_url,    provider_configs.base_url),
        status          = EXCLUDED.status,
        configured_at   = CASE
                            WHEN EXCLUDED.ai_key_id IS NOT NULL
                              AND provider_configs.configured_at IS NULL
                            THEN now()
                            ELSE provider_configs.configured_at
                          END,
        last_checked_at = CASE
                            WHEN EXCLUDED.status IN ('healthy', 'error')
                            THEN now()
                            ELSE provider_configs.last_checked_at
                          END,
        updated_at      = now()
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") IS 'Upserts a provider config row. ai_key_id: COALESCE-preserves existing key reference if not passed (health-check updates). base_url: COALESCE-preserves existing value if not passed. configured_at: set only on first key association (not overwritten on re-health-check). last_checked_at: updated whenever status is healthy or error. Called by authenticated users (after fn_store_api_key) and by test-provider Edge Function (service_role).';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "source_node_id" "uuid", "target_node_id" "uuid", "source_output_key" "text", "target_param_label" "text")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller does not own workflow %', p_workflow_id
      USING ERRCODE = '42501';
  END IF;

  -- Full replace: delete all existing edges for this workflow
  DELETE FROM lenses.workflow_edges
  WHERE lenses.workflow_edges.workflow_id = p_workflow_id;

  -- Insert incoming edges (skip if array is empty)
  IF jsonb_array_length(p_edges) > 0 THEN
    INSERT INTO lenses.workflow_edges (
      id,
      workflow_id,
      source_node_id,
      target_node_id,
      source_output_key,
      target_param_label
    )
    SELECT
      COALESCE((e->>'id')::uuid, gen_random_uuid()),
      p_workflow_id,
      (e->>'source_node_id')::uuid,
      (e->>'target_node_id')::uuid,
      COALESCE(e->>'source_output_key', 'output'),
      e->>'target_param_label'
    FROM jsonb_array_elements(p_edges) AS e;
  END IF;

  -- Return ALL edges for the workflow (not just the ones we inserted)
  RETURN QUERY
    SELECT
      we.id,
      we.workflow_id,
      we.source_node_id,
      we.target_node_id,
      we.source_output_key,
      we.target_param_label
    FROM lenses.workflow_edges we
    WHERE we.workflow_id = p_workflow_id;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") IS 'Bulk-upserts workflow edges for a workflow the caller owns.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "lens_id" "uuid", "version_id" "uuid", "position_x" double precision, "position_y" double precision, "label" "text", "ordinal" integer, "config" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'lensers', 'public'
    AS $$
DECLARE
  v_caller_id uuid;
BEGIN
  v_caller_id := lensers.get_auth_lenser_id();

  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = v_caller_id
  ) THEN
    RAISE EXCEPTION 'Unauthorized: caller does not own workflow %', p_workflow_id
      USING ERRCODE = '42501';
  END IF;

  RETURN QUERY
  INSERT INTO lenses.workflow_nodes (
    id,
    workflow_id,
    lens_id,
    version_id,
    position_x,
    position_y,
    label,
    ordinal,
    config
  )
  SELECT
    COALESCE((n->>'id')::uuid, gen_random_uuid()),
    p_workflow_id,
    (n->>'lens_id')::uuid,
    (n->>'version_id')::uuid,
    COALESCE((n->>'position_x')::float, 0),
    COALESCE((n->>'position_y')::float, 0),
    n->>'label',
    COALESCE((n->>'ordinal')::integer, 0),
    COALESCE(n->'config', '{}'::jsonb)
  FROM jsonb_array_elements(p_nodes) AS n
  ON CONFLICT ON CONSTRAINT workflow_nodes_pkey DO UPDATE SET
    position_x = EXCLUDED.position_x,
    position_y = EXCLUDED.position_y,
    label      = EXCLUDED.label,
    ordinal    = EXCLUDED.ordinal,
    version_id = EXCLUDED.version_id,
    config     = EXCLUDED.config
  RETURNING
    lenses.workflow_nodes.id,
    lenses.workflow_nodes.workflow_id,
    lenses.workflow_nodes.lens_id,
    lenses.workflow_nodes.version_id,
    lenses.workflow_nodes.position_x,
    lenses.workflow_nodes.position_y,
    lenses.workflow_nodes.label,
    lenses.workflow_nodes.ordinal,
    lenses.workflow_nodes.config,
    lenses.workflow_nodes.created_at;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") IS 'Bulk-upserts workflow nodes for a workflow the caller owns. p_nodes is a JSON array matching UpsertNodeInput shape. Now includes config JSONB for per-node model and parameter overrides.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text" DEFAULT NULL::"text", "p_ordinal" integer DEFAULT 0, "p_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'upsert_workflow_phase_forbidden' USING ERRCODE = '42501';
  END IF;

  IF p_id IS NOT NULL THEN
    UPDATE lenses.workflow_phases
    SET title = p_title, description = p_description, ordinal = p_ordinal, updated_at = now()
    WHERE id = p_id AND workflow_id = p_workflow_id
    RETURNING to_jsonb(lenses.workflow_phases.*) INTO v_result;
  END IF;

  IF v_result IS NULL THEN
    INSERT INTO lenses.workflow_phases (workflow_id, title, description, ordinal)
    VALUES (p_workflow_id, p_title, p_description, p_ordinal)
    RETURNING to_jsonb(lenses.workflow_phases.*) INTO v_result;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid" DEFAULT NULL::"uuid", "p_cron_expr" "text" DEFAULT '* * * * *'::"text", "p_timezone" "text" DEFAULT 'UTC'::"text", "p_description" "text" DEFAULT NULL::"text", "p_approval_policy" "jsonb" DEFAULT '{"requiresApproval": true}'::"jsonb", "p_is_active" boolean DEFAULT true, "p_global_model_id" "text" DEFAULT NULL::"text", "p_assignee_id" "uuid" DEFAULT NULL::"uuid", "p_workflow_assignment_id" "uuid" DEFAULT NULL::"uuid", "p_retry_policy" "jsonb" DEFAULT '{"maxRetries": 1}'::"jsonb", "p_failure_policy" "jsonb" DEFAULT '{"mode": "isolate"}'::"jsonb", "p_queue_policy" "jsonb" DEFAULT '{"mode": "parallel"}'::"jsonb", "p_inputs_template" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("id" "uuid", "workflow_id" "uuid", "cron_expr" "text", "timezone" "text", "description" "text", "is_active" boolean, "assignee_type" "text", "assignee_id" "uuid", "workflow_assignment_id" "uuid", "approval_policy" "jsonb", "retry_policy" "jsonb", "failure_policy" "jsonb", "queue_policy" "jsonb", "inputs_template" "jsonb", "global_model_id" "text", "next_run_at" timestamp with time zone, "last_run_at" timestamp with time zone, "last_run_id" "uuid", "last_dispatch_status" "text", "last_error_at" timestamp with time zone, "last_error_message" "text", "last_completed_at" timestamp with time zone, "last_result" "jsonb", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'agents', 'lensers', 'public'
    AS $$
DECLARE
  v_owner_id          uuid;
  v_active_id         uuid := lensers.get_auth_lenser_id();
  v_human_id          uuid := lensers.get_auth_human_lenser_id();
  v_ai_lenser_id      uuid;
  v_parts             text[];
  v_requires_approval boolean;
  v_spending_limit    numeric;
BEGIN
  -- Resolve workflow owner (human profile id)
  SELECT w.lenser_id INTO v_owner_id
  FROM lenses.workflows w
  WHERE w.id = p_workflow_id;

  -- Authorize when:
  --   a) Active session lenser owns the workflow (legacy human session), OR
  --   b) Human profile resolved from active session owns the workflow
  --      (agent workspace active; human still owns the workflow), OR
  --   c) Caller is an admin.
  IF v_owner_id IS NULL
     OR (v_owner_id <> v_active_id
         AND (v_human_id IS NULL OR v_owner_id <> v_human_id)
         AND NOT public.is_admin())
  THEN
    RAISE EXCEPTION 'not the owner of workflow %', p_workflow_id
      USING ERRCODE = '42501';
  END IF;

  -- CRON validation
  v_parts := regexp_split_to_array(trim(COALESCE(p_cron_expr, '')), '\s+');
  IF array_length(v_parts, 1) <> 5 THEN
    RAISE EXCEPTION 'Invalid CRON expression "%" — expected 5 fields', p_cron_expr
      USING ERRCODE = '22023';
  END IF;

  -- Cycle detection
  IF p_is_active AND lenses.fn_workflow_has_cycle(p_workflow_id) THEN
    RAISE EXCEPTION 'Cannot activate schedule for a workflow with cycles'
      USING ERRCODE = '22023', DETAIL = 'cycle_detected';
  END IF;

  -- Approval-bypass guard + audit (unchanged)
  IF p_is_active THEN
    v_requires_approval := COALESCE(
      (p_approval_policy->>'requiresApproval')::boolean,
      true
    );

    IF NOT v_requires_approval THEN
      SELECT al.id, pol.spending_limit_credits
        INTO v_ai_lenser_id, v_spending_limit
      FROM agents.ai_lensers al
      JOIN agents.policies pol ON pol.id = al.policy_id
      WHERE al.profile_id = v_owner_id
      LIMIT 1;

      IF v_spending_limit IS NULL THEN
        RAISE EXCEPTION
          'Cannot activate a schedule with requiresApproval=false and no spending_limit_credits. '
          'Set a spending limit on the agent policy before disabling approval gates.'
          USING ERRCODE = '23514';
      END IF;

      IF v_ai_lenser_id IS NOT NULL THEN
        INSERT INTO agents.action_logs (ai_lenser_id, action_type, result, metadata)
        VALUES (
          v_ai_lenser_id,
          'approval_bypass_attempted',
          'recorded',
          jsonb_build_object(
            'workflow_id',    p_workflow_id,
            'schedule_id',    p_schedule_id,
            'actor_lenser_id', v_owner_id,
            'spending_limit', v_spending_limit
          )
        );
      END IF;
    END IF;
  END IF;

  RETURN QUERY
  SELECT * FROM lenses.fn_upsert_workflow_schedule_internal(
    p_workflow_id, p_schedule_id, p_cron_expr, p_timezone, p_description,
    p_approval_policy, p_is_active, p_global_model_id, p_assignee_id,
    p_workflow_assignment_id, p_retry_policy, p_failure_policy,
    p_queue_policy, p_inputs_template
  );
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") IS 'Upsert a workflow schedule. Authorizes the active session lenser OR the resolved human owner OR an admin so the call succeeds after workspace switches.';



CREATE OR REPLACE FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text" DEFAULT NULL::"text", "p_output_type" "text" DEFAULT 'text'::"text", "p_model_hint" "text" DEFAULT NULL::"text", "p_ordinal" integer DEFAULT 0, "p_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses', 'lensers'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM lenses.workflows w
    WHERE w.id = p_workflow_id AND w.lenser_id = lensers.get_auth_lenser_id()
  ) THEN
    RAISE EXCEPTION 'upsert_workflow_task_forbidden' USING ERRCODE = '42501';
  END IF;

  IF p_id IS NOT NULL THEN
    UPDATE lenses.workflow_tasks
    SET title = p_title, prompt_text = p_prompt_text, output_type = p_output_type,
        model_hint = p_model_hint, ordinal = p_ordinal, updated_at = now()
    WHERE id = p_id AND phase_id = p_phase_id
    RETURNING to_jsonb(lenses.workflow_tasks.*) INTO v_result;
  END IF;

  IF v_result IS NULL THEN
    INSERT INTO lenses.workflow_tasks
      (phase_id, workflow_id, title, prompt_text, output_type, model_hint, ordinal)
    VALUES (p_phase_id, p_workflow_id, p_title, p_prompt_text, p_output_type, p_model_hint, p_ordinal)
    RETURNING to_jsonb(lenses.workflow_tasks.*) INTO v_result;
  END IF;

  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents', 'lensers'
    AS $_$
DECLARE
  v_lenser_id uuid := lensers.get_auth_human_lenser_id();
  v_predicate text;
  v_result    jsonb;
BEGIN
  IF p_table_name NOT IN (
    'teams', 'team_members', 'team_edges', 'personality_profiles',
    'memory_profiles', 'tool_profiles', 'model_profiles',
    'workflow_assignments', 'scratchpad_runs', 'workspace_settings',
    'evaluation_cases', 'evaluation_rubrics', 'evaluation_baselines',
    'tools_registry', 'tool_assignments', 'evaluations',
    'agent_run_events', 'agent_run_steps'
  ) THEN
    RAISE EXCEPTION 'upsert_forbidden: table % not in allowlist', p_table_name
      USING ERRCODE = '42501';
  END IF;

  v_predicate := public.fn__workspace_ownership_predicate(p_table_name);
  IF v_predicate IS NULL THEN
    RAISE EXCEPTION 'upsert_forbidden: no ownership predicate for %', p_table_name
      USING ERRCODE = '42501';
  END IF;

  EXECUTE format(
    'UPDATE agents.%I tbl SET updated_at = now() '
    'WHERE tbl.id = $1 AND (%s OR public.is_admin()) '
    'RETURNING to_jsonb(tbl.*)',
    p_table_name,
    v_predicate
  ) USING p_id, v_lenser_id INTO v_result;

  IF v_result IS NULL THEN
    RAISE EXCEPTION 'update_not_found_or_forbidden: record % in table % not found or not owned',
      p_id, p_table_name USING ERRCODE = '42501';
  END IF;

  RETURN v_result;
END;
$_$;


ALTER FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") IS 'Update any agents workspace record by id. Table name is whitelisted. Ownership enforced via human lenser id resolved through agents.ownerships (works after workspace switching). No reference to non-existent owner_lenser_id column on agent tables.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text" DEFAULT NULL::"text") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  RETURN battles.fn_move_battle_job_to_dlq(p_job_id, p_error_code, p_error_msg);
END;
$$;


ALTER FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") RETURNS TABLE("job_id" "uuid", "battle_id" "uuid", "contender_id" "uuid", "slot" "text", "task_prompt" "text", "provider_key" "text", "model_key" "text", "byok_key_ref_id" "text", "lens_id" "uuid", "version_id" "uuid", "max_tokens" integer, "temperature" numeric, "retry_count" integer, "ai_lenser_id" "uuid", "personality_note" "text", "personality_version_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM battles.fn_claim_battle_execution_job(p_worker_id);
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_queued_run"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
DECLARE
  v_result jsonb;
BEGIN
  SELECT to_jsonb(r.*) INTO v_result
  FROM execution.fn_claim_queued_run() r;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_queued_run"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_claim_queued_run"() IS 'Worker-only: claim the next queued execution run. Returns the claimed run as jsonb.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") RETURNS TABLE("run_id" "uuid", "workflow_id" "uuid", "schedule_id" "uuid", "inputs" "jsonb", "model_id" "uuid", "ai_lenser_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
BEGIN
  RETURN QUERY SELECT * FROM lenses.fn_claim_scheduled_workflow_run(p_worker_id);
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") IS 'Worker-only: claim the next pending scheduled workflow run. Delegates to lenses.fn_claim_scheduled_workflow_run.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_claim_team_run"() RETURNS "jsonb"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_run jsonb;
BEGIN
  UPDATE agents.team_runs
  SET status     = 'running',
      started_at = COALESCE(started_at, now()),
      updated_at = now()
  WHERE id = (
    SELECT id FROM agents.team_runs
    WHERE status = 'pending'
    ORDER BY created_at
    LIMIT 1
    FOR UPDATE SKIP LOCKED
  )
  RETURNING to_jsonb(agents.team_runs.*) INTO v_run;

  RETURN v_run;
END;
$$;


ALTER FUNCTION "public"."fn_worker_claim_team_run"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text" DEFAULT NULL::"text", "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_complete_battle_execution_job(p_job_id, p_status, p_output_text, p_error);
END;
$$;


ALTER FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer DEFAULT NULL::integer, "p_token_output" integer DEFAULT NULL::integer, "p_credit_cost" bigint DEFAULT NULL::bigint, "p_billing_status" "text" DEFAULT NULL::"text", "p_response_text" "text" DEFAULT NULL::"text", "p_response_meta" "jsonb" DEFAULT NULL::"jsonb", "p_error_code" "text" DEFAULT NULL::"text", "p_error_message" "text" DEFAULT NULL::"text", "p_latency_ms" integer DEFAULT NULL::integer) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
BEGIN
  PERFORM execution.fn_complete_execution_run(
    p_run_id, p_status, p_token_input, p_token_output, p_credit_cost,
    p_billing_status, p_response_text, p_response_meta,
    p_error_code, p_error_message, p_latency_ms
  );
END;
$$;


ALTER FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) IS 'Worker-only: mark an execution run terminal (completed or failed). Delegates to execution.fn_complete_execution_run.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
DECLARE
  v_result text;
BEGIN
  SELECT ai.fn_decrypt_api_key(p_key_id) INTO v_result;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") IS 'Worker-only: decrypt a stored BYOK API key. Delegates to ai.fn_decrypt_api_key.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text" DEFAULT 'provider_failed'::"text", "p_error_message" "text" DEFAULT 'Provider reported failure'::"text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
  UPDATE execution.runs
  SET    status        = 'failed',
         completed_at  = now(),
         error_code    = p_error_code,
         error_message = p_error_message
  WHERE  id = p_run_id;
$$;


ALTER FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") IS 'Worker-only: mark an execution run as failed with an error code and message.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai', 'vault', 'lensers'
    AS $$
DECLARE
  v_lenser_id    uuid;
  v_encrypted_id uuid;
  v_decrypted    text;
BEGIN
  IF p_ai_key_id IS NULL OR p_user_id IS NULL THEN
    RAISE EXCEPTION 'p_ai_key_id and p_user_id are required';
  END IF;

  -- Resolve the human profile for the authenticated user; ai.keys are owned
  -- by the human profile (not an agent workspace profile).
  SELECT p.id INTO v_lenser_id
  FROM lensers.profiles p
  WHERE p.user_id = p_user_id
    AND p.type = 'human'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'Key owner profile not found';
  END IF;

  -- Ownership check: key must belong to caller AND be active.
  SELECT k.encrypted_key_id INTO v_encrypted_id
  FROM ai.keys k
  WHERE k.id = p_ai_key_id
    AND k.lenser_id = v_lenser_id
    AND k.is_active = true
  LIMIT 1;

  IF v_encrypted_id IS NULL THEN
    -- Generic message: do not disclose whether the key exists for another user.
    RAISE EXCEPTION 'Key not found, revoked, or not owned by caller';
  END IF;

  SELECT decrypted_secret INTO v_decrypted
  FROM vault.decrypted_secrets
  WHERE id = v_encrypted_id;

  IF v_decrypted IS NULL THEN
    RAISE EXCEPTION 'Failed to decrypt key from vault';
  END IF;

  RETURN v_decrypted;
END;
$$;


ALTER FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") IS 'Worker-only: resolve and decrypt a BYOK API key from ai.keys via vault.decrypted_secrets. Requires p_user_id (the authenticated caller''s auth.uid()) and verifies the key belongs to that user. Patched 2026-05-16 to close cross-user IDOR.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "title" "text", "task_prompt" "text", "ai_judge_model_key" "text", "ai_judge_prompt" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT
    b.id,
    b.title,
    b.task_prompt,
    b.ai_judge_model_key,
    b.ai_judge_prompt
  FROM battles.battles b
  WHERE b.id = p_battle_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") IS 'CU: Returns battle fields required by the ai-judge-battle edge function. service_role only — replaces the invalid direct PostgREST table access that was silently returning empty results because the battles schema is not REST-exposed.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") RETURNS TABLE("id" "uuid", "slug" "text", "title" "text", "status" "text", "task_prompt" "text", "battle_type" "text", "total_vote_count" integer, "finalized_at" timestamp with time zone, "winner_contender_id" "uuid", "published_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT b.id, b.slug, b.title, b.status, b.task_prompt, b.battle_type,
         b.total_vote_count, b.finalized_at, b.winner_contender_id, b.published_at
  FROM battles.battles b
  WHERE b.id = p_battle_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT jsonb_build_object(
    'team_run', to_jsonb(tr.*),
    'team',     to_jsonb(t.*),
    'members',  COALESCE((
      SELECT jsonb_agg(to_jsonb(tm.*) ORDER BY tm.sort_order)
      FROM agents.team_members tm WHERE tm.team_id = tr.team_id
    ), '[]'::jsonb)
  )
  FROM agents.team_runs tr
  LEFT JOIN agents.teams t ON t.id = tr.team_id
  WHERE tr.id = p_team_run_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid" DEFAULT NULL::"uuid") RETURNS "text"
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_body text;
BEGIN
  IF p_version_id IS NOT NULL THEN
    SELECT v.template_body INTO v_body
    FROM lenses.versions v
    WHERE v.id = p_version_id
      AND v.lens_id = p_lens_id
    LIMIT 1;
    RETURN v_body;
  END IF;

  SELECT v.template_body INTO v_body
  FROM lenses.versions v
  WHERE v.lens_id = p_lens_id
    AND v.status = 'published'::content.content_status
  ORDER BY v.published_at DESC NULLS LAST, v.version_number DESC
  LIMIT 1;

  IF v_body IS NULL THEN
    SELECT v.template_body INTO v_body
    FROM lenses.versions v
    WHERE v.lens_id = p_lens_id
    ORDER BY v.version_number DESC
    LIMIT 1;
  END IF;

  RETURN v_body;
END;
$$;


ALTER FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") IS 'Worker-only: returns lenses.versions.template_body for workflow DAG execution (raw [[placeholders]]). When p_version_id is NULL, prefers latest published version for the lens.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT to_jsonb(tr.*)
  FROM agents.team_runs tr
  WHERE tr.id = p_team_run_id
  LIMIT 1;
$$;


ALTER FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles', 'reputation'
    AS $$
  SELECT jsonb_build_object(
    'battle_status',  b.status,
    'vote_count',     (
      SELECT COUNT(*)
      FROM battles.votes v
      WHERE v.battle_id = p_battle_id
    ),
    'lenser_score',   (
      SELECT ls.score
      FROM reputation.lenser_scores ls
      WHERE ls.lenser_id  = p_lenser_id
        AND ls.score_type = 'composite'
      ORDER BY ls.computed_at DESC
      LIMIT 1
    ),
    'lenser_elo',     (
      SELECT cr.elo_rating
      FROM reputation.contender_ratings cr
      WHERE cr.lenser_id = p_lenser_id
      ORDER BY cr.updated_at DESC
      LIMIT 1
    )
  )
  FROM battles.battles b
  WHERE b.id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") IS 'Worker-only: return battle status, total vote count, and lenser scores as jsonb for the score-vote-risk edge function. Single round-trip.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) RETURNS TABLE("recent_count" bigint, "total_count" bigint, "draw_count" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  SELECT
    COUNT(*) FILTER (WHERE v.created_at >= p_since_ts) AS recent_count,
    COUNT(*)                                            AS total_count,
    COUNT(*) FILTER (WHERE v.is_draw = true)            AS draw_count
  FROM battles.votes v
  WHERE v.voter_lenser_id = p_voter_lenser_id;
$$;


ALTER FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) IS 'Worker-only: return recent/total/draw vote counts for a voter in one query.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") RETURNS TABLE("workflow_id" "uuid", "triggered_by" "uuid")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT wr.workflow_id, wr.triggered_by
  FROM lenses.workflow_runs wr
  WHERE wr.id = p_run_id;
$$;


ALTER FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") IS 'Worker-only: return workflow_id and triggered_by for a workflow run.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") RETURNS "jsonb"
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT jsonb_build_object(
    'nodes', COALESCE(
      (SELECT jsonb_agg(
         jsonb_build_object(
           'id',         n.id,
           'lens_id',    n.lens_id,
           'version_id', n.version_id,
           'config',     n.config
         ) ORDER BY n.ordinal
       )
       FROM lenses.workflow_nodes n
       WHERE n.workflow_id = p_workflow_id
      ), '[]'::jsonb
    ),
    'edges', COALESCE(
      (SELECT jsonb_agg(
         jsonb_build_object(
           'id',                e.id,
           'source_node_id',    e.source_node_id,
           'target_node_id',    e.target_node_id,
           'source_output_key', e.source_output_key,
           'target_param_label',e.target_param_label
         )
       )
       FROM lenses.workflow_edges e
       WHERE e.workflow_id = p_workflow_id
      ), '[]'::jsonb
    )
  );
$$;


ALTER FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") IS 'Worker-only: return workflow nodes and edges as { nodes: [...], edges: [...] }. Reduces two round-trips to one.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") RETURNS "uuid"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'media'
    AS $$
  INSERT INTO media.objects
    (workspace_id, owner_lenser_id, external_url, mime_type, media_type,
     name, visibility, lifecycle_state, metadata)
  VALUES (
    p_workspace_id, p_owner_lenser_id, p_external_url, p_mime_type, p_media_type,
    p_name, 'private', 'active',
    jsonb_build_object('workflow_run_id', p_run_id, 'node_id', p_node_id)
  )
  RETURNING id;
$$;


ALTER FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") IS 'Worker-only: insert a media.objects row for output produced by a workflow node. Tags the object with workflow_run_id and node_id in metadata.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text" DEFAULT NULL::"text", "p_content_json" "jsonb" DEFAULT NULL::"jsonb", "p_media_ids" "uuid"[] DEFAULT NULL::"uuid"[]) RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'execution'
    AS $$
BEGIN
  PERFORM execution.fn_persist_execution_artifacts(
    p_run_id, p_lenser_id, p_workspace_id, p_ai_model_id,
    p_kind, p_content_text, p_content_json, p_media_ids
  );
END;
$$;


ALTER FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) IS 'Worker-only: persist execution artifacts to the DB. Delegates to execution.fn_persist_execution_artifacts.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "text"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
DECLARE
  v_result text;
BEGIN
  SELECT lenses.fn_render_template(p_version_id, p_inputs) INTO v_result;
  RETURN v_result;
END;
$$;


ALTER FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") IS 'Worker-only: render a lens version template with the supplied inputs. Delegates to lenses.fn_render_template.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
BEGIN
  PERFORM battles.fn_requeue_battle_job_with_backoff(p_job_id, p_backoff_ms, p_error);
END;
$$;


ALTER FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_run_auto_promote_cycle"() RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'battles', 'public'
    AS $$
DECLARE
  r         RECORD;
  promoted  integer := 0;
BEGIN
  FOR r IN
    SELECT id
    FROM   battles.battles
    WHERE  status      = 'draft'
      AND  auto_promote = TRUE
  LOOP
    BEGIN
      IF public.fn_battles_auto_promote(r.id) THEN
        promoted := promoted + 1;
      END IF;
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'fn_worker_run_auto_promote_cycle: battle % failed: %', r.id, SQLERRM;
    END;
  END LOOP;

  RETURN promoted;
END;
$$;


ALTER FUNCTION "public"."fn_worker_run_auto_promote_cycle"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() IS 'CU: Wraps the auto-promote sweep for the Node.js worker. Replaces the direct svc.from(''battles'') query that targeted public.battles (which does not exist; the table is in battles.battles, not REST-exposed). Returns the number of battles promoted draft → open this cycle.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
  UPDATE battles.battles
  SET og_image_url = p_og_image_url
  WHERE id = p_battle_id;
$$;


ALTER FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") IS 'Worker-only: set og_image_url on a battle. Called by the generate-battle-og-image edge function.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid" DEFAULT NULL::"uuid", "p_byok_key_ref_id" "uuid" DEFAULT NULL::"uuid", "p_input_snapshot" "jsonb" DEFAULT '{}'::"jsonb") RETURNS TABLE("request_id" "uuid", "run_id" "uuid")
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'execution', 'tenancy'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_user_id IS NULL THEN
    RAISE EXCEPTION 'p_user_id is required';
  END IF;

  -- Resolve the human profile for this auth user.
  SELECT id INTO v_lenser_id
  FROM lensers.profiles
  WHERE user_id = p_user_id
    AND type    = 'human'
  LIMIT 1;

  IF v_lenser_id IS NULL THEN
    RAISE EXCEPTION 'No human lenser profile found for user';
  END IF;

  RETURN QUERY
    SELECT r.request_id, r.run_id
    FROM execution.fn_start_execution(
      p_lenser_id       => v_lenser_id,
      p_origin_type     => p_origin_type,
      p_funding_source  => p_funding_source,
      p_model_id        => NULL,
      p_lens_id         => p_lens_id,
      p_byok_key_ref_id => p_byok_key_ref_id,
      p_input_snapshot  => p_input_snapshot
    ) r;
END;
$$;


ALTER FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") IS 'Edge-function gateway: resolves auth user → lenser profile, then delegates to execution.fn_start_execution. Accepts p_user_id so the edge function never touches private schemas through PostgREST. Callable only by service_role.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  UPDATE agents.team_runs
  SET status       = p_status,
      completed_at = COALESCE(p_completed_at, CASE WHEN p_status IN ('completed','failed','cancelled') THEN now() ELSE NULL END),
      updated_at   = now()
  WHERE id = p_team_run_id;
$$;


ALTER FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone DEFAULT NULL::timestamp with time zone) RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
  SELECT public.fn_worker_update_team_run(p_team_run_id, p_status, p_completed_at);
$$;


ALTER FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") RETURNS "void"
    LANGUAGE "sql" SECURITY DEFINER
    SET "search_path" TO 'public', 'reputation'
    AS $$
  UPDATE reputation.vote_risk_scores
  SET risk_score    = p_risk_score,
      risk_factors  = p_risk_factors,
      review_status = p_review_status
  WHERE vote_id = p_vote_id;
$$;


ALTER FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") IS 'Worker-only: update risk_score, risk_factors, and review_status on a vote_risk_scores row.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text" DEFAULT NULL::"text", "p_execution_run_id" "uuid" DEFAULT NULL::"uuid", "p_artifact_id" "uuid" DEFAULT NULL::"uuid", "p_is_final" boolean DEFAULT true, "p_media_url" "text" DEFAULT NULL::"text", "p_mime_type" "text" DEFAULT NULL::"text", "p_output_modality" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'battles'
    AS $$
DECLARE
  v_status        text;
  v_content_url   text;
  v_submitted_at  timestamptz;
BEGIN
  -- Determine status and whether we have a final value to submit.
  -- Async-pending: media_url is NULL (task not yet polled), artifact_id holds
  -- the providerTaskId for the poll worker.
  IF p_content_text IS NOT NULL OR p_media_url IS NOT NULL THEN
    v_status       := 'submitted';
    v_submitted_at := now();
    -- Mirror media_url into content_url to satisfy submissions_content_required.
    v_content_url  := p_media_url;
  ELSE
    -- No content yet: async media task is pending.
    v_status       := 'pending';
    v_submitted_at := NULL;
    v_content_url  := NULL;
  END IF;

  INSERT INTO battles.submissions (
    battle_id,
    contender_id,
    content_text,
    content_url,
    media_url,
    mime_type,
    output_modality,
    execution_run_id,
    artifact_id,
    is_final,
    status,
    source_type,
    submitted_at
  ) VALUES (
    p_battle_id,
    p_contender_id,
    p_content_text,
    v_content_url,
    p_media_url,
    p_mime_type,
    COALESCE(p_output_modality, CASE WHEN p_content_text IS NOT NULL THEN 'text' ELSE NULL END),
    p_execution_run_id,
    p_artifact_id,
    p_is_final,
    v_status,
    'execution_output',
    v_submitted_at
  )
  ON CONFLICT (battle_id, contender_id) DO UPDATE
    SET content_text      = COALESCE(EXCLUDED.content_text,      battles.submissions.content_text),
        content_url       = COALESCE(EXCLUDED.content_url,       battles.submissions.content_url),
        media_url         = COALESCE(EXCLUDED.media_url,         battles.submissions.media_url),
        mime_type         = COALESCE(EXCLUDED.mime_type,         battles.submissions.mime_type),
        output_modality   = COALESCE(EXCLUDED.output_modality,   battles.submissions.output_modality),
        execution_run_id  = COALESCE(EXCLUDED.execution_run_id,  battles.submissions.execution_run_id),
        artifact_id       = COALESCE(EXCLUDED.artifact_id,       battles.submissions.artifact_id),
        is_final          = EXCLUDED.is_final,
        status            = EXCLUDED.status,
        submitted_at      = COALESCE(battles.submissions.submitted_at, EXCLUDED.submitted_at),
        updated_at        = now();
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") IS 'Worker-only (service_role): upsert a battle submission for an AI contender. Handles text, sync media (image/audio), and async-pending media (video/music) results. Does not check auth.uid() — callers must run as service_role.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'platform'
    AS $$
BEGIN
  PERFORM platform.fn_upsert_worker_heartbeat(p_worker_id, p_worker_type, p_metadata);
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") IS 'Worker-only: upsert a worker heartbeat record. Delegates to platform.fn_upsert_worker_heartbeat.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
DECLARE
  v_terminal text[] := ARRAY['completed', 'failed', 'timed_out', 'cancelled'];
BEGIN
  INSERT INTO lenses.workflow_node_results
    (run_id, node_id, status, output_data, error_message, started_at, completed_at)
  VALUES (
    p_run_id, p_node_id, p_status, p_output_data, p_error_message,
    CASE WHEN p_status = 'running' THEN now() ELSE NULL END,
    CASE WHEN p_status = ANY(v_terminal) THEN now() ELSE NULL END
  )
  ON CONFLICT (run_id, node_id) DO UPDATE
    SET status        = EXCLUDED.status,
        output_data   = COALESCE(EXCLUDED.output_data,   lenses.workflow_node_results.output_data),
        error_message = COALESCE(EXCLUDED.error_message, lenses.workflow_node_results.error_message),
        started_at    = COALESCE(lenses.workflow_node_results.started_at, EXCLUDED.started_at),
        completed_at  = CASE
          WHEN EXCLUDED.status = ANY(v_terminal) THEN now()
          ELSE lenses.workflow_node_results.completed_at
        END;
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") IS 'Worker-only: upsert a workflow node execution result. Sets started_at on first "running" write; sets completed_at on terminal status. Preserves existing output_data/error_message if new value is NULL.';



CREATE OR REPLACE FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb" DEFAULT NULL::"jsonb", "p_error_message" "text" DEFAULT NULL::"text", "p_resolved_input_snapshot" "jsonb" DEFAULT NULL::"jsonb", "p_provider_route" "jsonb" DEFAULT NULL::"jsonb") RETURNS "void"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
DECLARE
  v_terminal text[] := ARRAY['completed', 'failed', 'timed_out', 'cancelled', 'blocked', 'invalidated', 'skipped'];
BEGIN
  INSERT INTO lenses.workflow_node_results (
    run_id, node_id, status, output_data, error_message,
    resolved_input_snapshot, provider_route,
    started_at, completed_at
  )
  VALUES (
    p_run_id, p_node_id, p_status, p_output_data, p_error_message,
    p_resolved_input_snapshot, p_provider_route,
    CASE WHEN p_status IN ('running', 'streaming', 'retrying') THEN now() ELSE NULL END,
    CASE WHEN p_status = ANY(v_terminal) THEN now() ELSE NULL END
  )
  ON CONFLICT (run_id, node_id) DO UPDATE
    SET status        = EXCLUDED.status,
        output_data   = COALESCE(EXCLUDED.output_data, lenses.workflow_node_results.output_data),
        error_message = COALESCE(EXCLUDED.error_message, lenses.workflow_node_results.error_message),
        resolved_input_snapshot = COALESCE(
          EXCLUDED.resolved_input_snapshot,
          lenses.workflow_node_results.resolved_input_snapshot
        ),
        provider_route = COALESCE(EXCLUDED.provider_route, lenses.workflow_node_results.provider_route),
        started_at    = COALESCE(lenses.workflow_node_results.started_at, EXCLUDED.started_at),
        completed_at  = CASE
          WHEN EXCLUDED.status = ANY(v_terminal) THEN now()
          ELSE lenses.workflow_node_results.completed_at
        END;
END;
$$;


ALTER FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") IS 'Worker-only: upsert workflow_node_results with full jsonb output_data and optional resolved_input_snapshot / provider_route.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_child_run_id UUID;
BEGIN
  IF NOT EXISTS (SELECT 1 FROM lenses.workflow_runs WHERE id = p_parent_run_id) THEN
    RAISE EXCEPTION 'Parent run not found' USING ERRCODE = 'P0002';
  END IF;

  INSERT INTO lenses.workflow_runs
    (workflow_id, triggered_by, status, context_inputs, trigger_mode)
  VALUES
    (p_child_workflow_id, NULL, 'pending', p_input, 'schedule')
  RETURNING id INTO v_child_run_id;

  INSERT INTO lenses.workflow_run_chains
    (parent_run_id, child_run_id, chain_reason)
  VALUES
    (p_parent_run_id, v_child_run_id, 'chained_run');

  RETURN v_child_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") IS 'CD: Creates a child run and records the parent→child chain relationship.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  rec       RECORD;
  dispatched INT := 0;
BEGIN
  FOR rec IN
    SELECT wt.id AS trigger_id, wt.workflow_id, wt.owner_id
      FROM lenses.workflow_triggers wt
     WHERE wt.enabled = TRUE
       AND wt.trigger_type = p_event_type
       AND public.fn_workflows_evaluate_condition(wt.condition, p_event_payload)
  LOOP
    INSERT INTO lenses.workflow_runs
      (workflow_id, triggered_by, status, context_inputs, trigger_mode)
    VALUES
      (rec.workflow_id, NULL, 'pending', p_event_payload, 'schedule');

    -- Update last_fired_at
    UPDATE lenses.workflow_triggers
       SET last_fired_at = now()
     WHERE id = rec.trigger_id;

    dispatched := dispatched + 1;
  END LOOP;

  RETURN dispatched;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") IS 'CD: Fan-out to enabled triggers matching event_type + condition. Returns dispatched count.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") RETURNS boolean
    LANGUAGE "plpgsql" IMMUTABLE SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
DECLARE
  k TEXT;
  v JSONB;
BEGIN
  -- Empty condition always matches
  IF p_condition = '{}'::JSONB OR p_condition IS NULL THEN
    RETURN TRUE;
  END IF;

  -- Each key in p_condition must match the corresponding key in p_event
  FOR k, v IN SELECT * FROM jsonb_each(p_condition)
  LOOP
    IF p_event -> k IS DISTINCT FROM v THEN
      RETURN FALSE;
    END IF;
  END LOOP;

  RETURN TRUE;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") IS 'CD: IMMUTABLE shallow key-match condition evaluator. Used by trigger fan-out.';



CREATE OR REPLACE FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer DEFAULT 0, "p_limit" integer DEFAULT 12, "p_search" "text" DEFAULT NULL::"text") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "title" "text", "description" "text", "visibility" "text", "battle_count" integer, "node_count" bigint, "reaction_totals" "jsonb", "fork_count" integer, "parent_workflow_id" "uuid", "created_at" timestamp with time zone, "updated_at" timestamp with time zone, "hot_score" double precision)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'lenses'
    AS $$
  SELECT
    w.id,
    w.lenser_id,
    w.title,
    w.description,
    w.visibility::text,
    w.battle_count,
    (
      SELECT count(*)
      FROM lenses.workflow_nodes wn
      WHERE wn.workflow_id = w.id
    ) AS node_count,
    w.reaction_totals,
    w.fork_count,
    w.parent_workflow_id,
    w.created_at,
    w.updated_at,
    log(
      GREATEST(
        1,
        5.0 * w.battle_count
        + 3.0 * w.fork_count
        + 4.0 * COALESCE((w.reaction_totals->>'copy')::int, 0)
        + 2.0 * COALESCE((w.reaction_totals->>'like')::int, 0)
        + 1.0 * COALESCE((w.reaction_totals->>'saved')::int, 0)
      )
    ) / NULLIF(
      pow((extract(epoch FROM now() - w.created_at) / 3600.0 + 2), 1.5),
      0
    ) AS hot_score
  FROM lenses.workflows w
  WHERE w.visibility::text = 'public'
    AND (p_search IS NULL OR w.title ILIKE '%' || p_search || '%')
  ORDER BY hot_score DESC, w.created_at DESC
  LIMIT p_limit
  OFFSET p_offset;
$$;


ALTER FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'lenses', 'public'
    AS $$
DECLARE
  v_stored_secret TEXT;
  v_run_id        UUID;
BEGIN
  -- Find the webhook trigger for this workflow
  SELECT webhook_secret INTO v_stored_secret
    FROM lenses.workflow_triggers
   WHERE workflow_id   = p_workflow_id
     AND trigger_type  = 'webhook'
     AND enabled       = TRUE
   LIMIT 1;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'No active webhook trigger for workflow' USING ERRCODE = 'P0002';
  END IF;

  IF v_stored_secret IS DISTINCT FROM p_secret THEN
    RAISE EXCEPTION 'invalid_webhook_secret' USING ERRCODE = 'P0001';
  END IF;

  INSERT INTO lenses.workflow_runs
    (workflow_id, triggered_by, status, context_inputs, trigger_mode)
  VALUES
    (p_workflow_id, NULL, 'pending', p_payload, 'schedule')
  RETURNING id INTO v_run_id;

  RETURN v_run_id;
END;
$$;


ALTER FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") IS 'CD: HMAC-validated webhook dispatch. Raises P0001 invalid_webhook_secret on mismatch.';



CREATE OR REPLACE FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric DEFAULT 0.5, "p_expires_at" timestamp with time zone DEFAULT NULL::timestamp with time zone, "p_team_run_id" "uuid" DEFAULT NULL::"uuid", "p_metadata" "jsonb" DEFAULT '{}'::"jsonb") RETURNS "uuid"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'agents'
    AS $$
DECLARE
  v_profile agents.memory_profiles;
  v_id      uuid;
BEGIN
  SELECT * INTO v_profile FROM agents.memory_profiles WHERE id = p_profile_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'memory profile not found' USING ERRCODE = 'P0002';
  END IF;
  IF NOT agents.can_manage_ai_lenser(v_profile.ai_lenser_id) THEN
    RAISE EXCEPTION 'not authorized for this agent' USING ERRCODE = '42501';
  END IF;

  INSERT INTO agents.memories (
    profile_id, ai_lenser_id, scope, source, content,
    embedding_metadata, confidence, expires_at, team_run_id
  )
  VALUES (
    p_profile_id, v_profile.ai_lenser_id, p_scope, p_source, p_content,
    COALESCE(p_metadata, '{}'::jsonb),
    COALESCE(p_confidence, 0.5),
    p_expires_at,
    p_team_run_id
  )
  RETURNING id INTO v_id;

  INSERT INTO agents.memory_access_logs (memory_id, team_run_id, action, context)
  VALUES (v_id, p_team_run_id, 'write', jsonb_build_object('source', p_source, 'scope', p_scope));

  RETURN v_id;
END;
$$;


ALTER FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") RETURNS integer
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
DECLARE
  v_count INT;
BEGIN
  -- Restrict to service_role / supabase_admin
  IF current_setting('role', true) NOT IN ('service_role', 'supabase_admin') THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'fn_xp_freeze_content is restricted to service_role.',
            HINT    = 'permission_denied';
  END IF;

  -- Mark XP events as frozen in their meta JSONB without deleting the audit trail
  UPDATE xp.events
  SET meta = COALESCE(meta, '{}'::jsonb) || '{"frozen": true, "freeze_reason": "moderated_content"}'::jsonb
  WHERE source_ref_type = p_source_ref_type
    AND source_ref_id   = p_source_ref_id
    AND NOT COALESCE((meta->>'frozen')::boolean, false);

  GET DIAGNOSTICS v_count = ROW_COUNT;

  RAISE NOTICE 'fn_xp_freeze_content: froze % events for %/%', v_count, p_source_ref_type, p_source_ref_id;
  RETURN v_count;
END;
$$;


ALTER FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") IS 'CB: Marks XP events for moderated/deleted content as frozen. Preserves audit trail; service_role only.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_apps"() RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "difficulty" "text", "is_active" boolean)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp'
    AS $$
BEGIN
  RETURN QUERY
  SELECT a.id, a.slug::text, a.name::text, a.difficulty::text, a.is_active
  FROM xp.apps a
  WHERE a.is_active = true
  ORDER BY a.slug;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_apps"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_apps"() IS 'Returns all active XP app definitions.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("id" "uuid", "lenser_id" "uuid", "context" "text", "contribution_type" "text", "external_ref" "text", "title" "text", "verified_by" "uuid", "xp_event_id" "uuid", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_lenser_id IS NULL THEN
    SELECT id INTO v_lenser_id FROM lensers.profiles WHERE user_id = auth.uid();
  ELSE
    v_lenser_id := p_lenser_id;
  END IF;

  RETURN QUERY
  SELECT
    c.id, c.lenser_id, c.context::text, c.contribution_type::text,
    c.external_ref::text, c.title::text, c.verified_by, c.xp_event_id, c.created_at
  FROM xp.contributions c
  WHERE c.lenser_id = v_lenser_id
  ORDER BY c.created_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") IS 'Returns XP contributions for the given lenser or the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid" DEFAULT NULL::"uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "action_key" "text", "xp" integer, "base_xp" integer, "source" "text", "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_lenser_id IS NULL THEN
    SELECT id INTO v_lenser_id FROM lensers.profiles WHERE user_id = auth.uid();
  ELSE
    v_lenser_id := p_lenser_id;
  END IF;

  RETURN QUERY
  SELECT e.id, e.action_key::text, e.xp, e.base_xp, e.source::text, e.created_at
  FROM xp.events e
  WHERE e.lenser_id = v_lenser_id
  ORDER BY e.created_at DESC
  LIMIT LEAST(p_limit, 200);
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) IS 'Returns paginated XP event history for the given lenser or the caller.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer DEFAULT 20) RETURNS TABLE("id" "uuid", "old_level" integer, "new_level" integer, "total_xp_at" bigint, "created_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  IF auth.uid() IS DISTINCT FROM p_lenser_id
     AND current_setting('role', true) NOT IN ('service_role', 'supabase_admin')
  THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'You may only read your own level-up history.',
            HINT    = 'permission_denied';
  END IF;

  RETURN QUERY
  SELECT
    lu.id,
    lu.old_level,
    lu.new_level,
    lu.total_xp_at,
    lu.created_at
  FROM xp.level_ups lu
  WHERE lu.lenser_id = p_lenser_id
  ORDER BY lu.created_at DESC
  LIMIT LEAST(p_limit, 100);
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) IS 'CB: Returns level-up history for a lenser. SECURITY DEFINER — callers may only read own history.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid" DEFAULT '00000000-0000-0000-0000-000000000001'::"uuid") RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "description" "text", "reward_description" "text", "featured_challenges" "jsonb", "starts_at" timestamp with time zone, "ends_at" timestamp with time zone, "is_active" boolean, "status" "text")
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  RETURN QUERY
  SELECT
    s.id,
    s.slug,
    s.name,
    s.description,
    s.reward_description,
    s.featured_challenges,
    s.starts_at,
    s.ends_at,
    s.is_active,
    CASE
      WHEN s.is_active                    THEN 'active'
      WHEN s.starts_at > now()            THEN 'upcoming'
      ELSE                                     'ended'
    END AS status
  FROM xp.seasons s
  WHERE s.app_id = p_app_id
  ORDER BY s.starts_at DESC;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") IS 'CB: Returns all seasons for an app with computed status (active/upcoming/ended).';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_self"() RETURNS TABLE("lenser_id" "uuid", "total_xp" bigint, "current_level" integer)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'lensers', 'xp', 'auth'
    AS $$
begin
    return query
    select
        t.lenser_id,
        t.total_xp,
        t.current_level
    from xp.totals t
    join lensers.profiles p on p.id = t.lenser_id
    where p.user_id = auth.uid();
end;
$$;


ALTER FUNCTION "public"."fn_xp_get_self"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text" DEFAULT 'daily'::"text") RETURNS TABLE("lenser_id" "uuid", "streak_type" "text", "current_streak" integer, "best_streak" integer, "last_update_at" timestamp with time zone)
    LANGUAGE "plpgsql" STABLE SECURITY DEFINER
    SET "search_path" TO 'xp', 'public'
    AS $$
BEGIN
  -- RLS: callers may only read own streak; service_role reads any
  IF auth.uid() IS DISTINCT FROM p_lenser_id
     AND current_setting('role', true) NOT IN ('service_role', 'supabase_admin')
  THEN
    RAISE EXCEPTION 'permission_denied'
      USING ERRCODE = '42501',
            DETAIL  = 'You may only read your own streak.',
            HINT    = 'permission_denied';
  END IF;

  RETURN QUERY
  SELECT
    s.lenser_id,
    s.streak_type,
    s.current_streak,
    s.best_streak,
    s.last_update_at
  FROM xp.streaks s
  WHERE s.lenser_id    = p_lenser_id
    AND s.streak_type  = p_streak_type;
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") IS 'CB: Returns streak info for a lenser. SECURITY DEFINER — callers may only read own streak.';



CREATE OR REPLACE FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid" DEFAULT NULL::"uuid", "p_app_id" "uuid" DEFAULT NULL::"uuid") RETURNS TABLE("total_xp" bigint, "current_level" integer, "app_id" "uuid", "min_total_xp" bigint, "max_total_xp" bigint)
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public', 'xp', 'lensers', 'auth'
    AS $$
DECLARE
  v_lenser_id uuid;
BEGIN
  IF p_lenser_id IS NULL THEN
    SELECT id INTO v_lenser_id FROM lensers.profiles WHERE user_id = auth.uid();
  ELSE
    v_lenser_id := p_lenser_id;
  END IF;

  RETURN QUERY
  SELECT
    COALESCE(xrow.row_total_xp, 0)::bigint,
    COALESCE(xrow.row_level, 1),
    COALESCE(xrow.row_app_id, '00000000-0000-0000-0000-000000000001'::uuid),
    COALESCE(l.min_total_xp, 0)::bigint,
    l.max_total_xp::bigint
  FROM (
    SELECT xt.total_xp AS row_total_xp,
           xt.current_level AS row_level,
           xt.app_id AS row_app_id
    FROM xp.totals xt
    WHERE xt.lenser_id = v_lenser_id
      AND (p_app_id IS NULL OR xt.app_id = p_app_id)
    LIMIT 1
  ) xrow
  LEFT JOIN xp.levels l
    ON l.app_id = COALESCE(xrow.row_app_id, '00000000-0000-0000-0000-000000000001'::uuid)
   AND l.level  = COALESCE(xrow.row_level, 1);
END;
$$;


ALTER FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") IS 'Returns XP summary (total, level, level band) for the given lenser or the caller.';



CREATE OR REPLACE FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") RETURNS TABLE("id" "uuid", "name" "text", "key" "text", "provider_key" "text", "provider_name" "text", "context_window_tokens" integer, "supports_tools" boolean, "supports_json_schema" boolean, "supports_vision" boolean, "capabilities" "text"[], "created_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
    SELECT
        m.id,
        m.name,
        m.key,
        p.key               AS provider_key,
        p.display_name      AS provider_name,
        m.context_window_tokens,
        m.supports_tools,
        m.supports_json_schema,
        m.supports_vision,
        m.capabilities,
        m.created_at
    FROM  ai.models     m
    JOIN  ai.providers  p ON p.id = m.provider_id
    WHERE p.key       = p_provider_key
      AND p.is_active = true
      AND m.is_active = true
    ORDER BY m.name;
$$;


ALTER FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") OWNER TO "postgres";


COMMENT ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") IS 'Returns active models for a given active provider key. Column model_key renamed to key in migration 000059. SECURITY DEFINER — runs as postgres, bypasses RLS.';



CREATE OR REPLACE FUNCTION "public"."get_active_providers"() RETURNS TABLE("id" "uuid", "key" "text", "display_name" "text", "base_url" "text", "docs_url" "text", "metadata" "jsonb", "created_at" timestamp with time zone, "updated_at" timestamp with time zone)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'ai', 'public'
    AS $$
    SELECT
        p.id,
        p.key,
        p.display_name,
        p.base_url,
        p.docs_url,
        p.metadata,
        p.created_at,
        p.updated_at
    FROM ai.providers p
    WHERE p.is_active = true
      AND EXISTS (
          SELECT 1
          FROM ai.models m
          WHERE m.provider_id = p.id
            AND m.is_active   = true
      )
    ORDER BY p.display_name;
$$;


ALTER FUNCTION "public"."get_active_providers"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."get_active_providers"() IS 'Returns active AI providers that have at least one active model. Providers that are marked active but have no active models are excluded. SECURITY DEFINER — runs as postgres. Callable by anon and authenticated. ai.providers is not directly accessible by those roles.';



CREATE OR REPLACE FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") RETURNS TABLE("model_id" "uuid", "unit_type" "text")
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'ai'
    AS $$
    SELECT m.id AS model_id, mp.unit_type::text
    FROM ai.models m
    JOIN ai.providers p ON p.id = m.provider_id
    JOIN ai.model_pricing mp
        ON mp.model_id = m.id
       AND mp.effective_to IS NULL
    WHERE p.key = p_provider_key
      AND m.key = p_model_key
      AND m.is_active = true
    LIMIT 1;
$$;


ALTER FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."guard_notification_fields"() RETURNS "trigger"
    LANGUAGE "plpgsql"
    SET "search_path" TO ''
    AS $$
BEGIN
  -- Service-role and internal functions may update any column
  IF current_user IN ('postgres', 'supabase_admin', 'service_role') THEN
    RETURN NEW;
  END IF;

  -- Silently reset protected columns to their original values.
  -- Only read_at is allowed to change for authenticated users.
  NEW.id         := OLD.id;
  NEW.lenser_id  := OLD.lenser_id;
  NEW.type       := OLD.type;
  NEW.title      := OLD.title;
  NEW.body       := OLD.body;
  NEW.action_url := OLD.action_url;
  NEW.metadata   := OLD.metadata;
  NEW.created_at := OLD.created_at;

  RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."guard_notification_fields"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."is_admin"() RETURNS boolean
    LANGUAGE "sql" STABLE
    SET "search_path" TO 'public', 'lensers', 'auth'
    AS $$
  select coalesce((auth.jwt() ->> 'role') = 'admin', false);
$$;


ALTER FUNCTION "public"."is_admin"() OWNER TO "postgres";


CREATE OR REPLACE FUNCTION "public"."list_tags_stats"("p_lang" "text" DEFAULT 'en'::"text", "p_limit" integer DEFAULT 50, "p_offset" integer DEFAULT 0) RETURNS TABLE("id" "uuid", "slug" "text", "name" "text", "language_code" "text", "created_at" timestamp with time zone, "created_count" bigint, "viewed_count" bigint, "reacted_count" bigint, "total_usage" bigint, "trend_score" bigint)
    LANGUAGE "sql" STABLE SECURITY DEFINER
    SET "search_path" TO 'public', 'content', 'analytics'
    AS $$
WITH daily AS (
    SELECT tag_id,
           sum(created_count) AS created_total,
           sum(viewed_count)  AS viewed_total,
           sum(reacted_count) AS reacted_total
    FROM analytics.tag_activity_daily
    GROUP BY tag_id
), recent_7d AS (
    SELECT tag_id,
           sum(
               (created_count * 1) +
               (viewed_count  * 2) +
               (reacted_count * 3)
           ) AS trend_score_7d
    FROM analytics.tag_activity_daily
    WHERE activity_date >= CURRENT_DATE - INTERVAL '7 days'
    GROUP BY tag_id
)
SELECT
    t.id,
    t.slug,
    COALESCE(tn_lang.name, tn_any.name, t.slug) AS name,
    COALESCE(tn_lang.lang, tn_any.lang, 'slug')  AS language_code,
    t.created_at,
    COALESCE(d.created_total,  0) AS created_count,
    COALESCE(d.viewed_total,   0) AS viewed_count,
    COALESCE(d.reacted_total,  0) AS reacted_count,
    (COALESCE(d.created_total, 0) + COALESCE(d.viewed_total, 0) + COALESCE(d.reacted_total, 0)) AS total_usage,
    COALESCE(r.trend_score_7d, 0) AS trend_score
FROM content.tags t
LEFT JOIN LATERAL (
    SELECT name, language_code AS lang
    FROM content.tag_translations
    WHERE tag_id       = t.id
      AND language_code = p_lang
    LIMIT 1
) tn_lang ON true
LEFT JOIN LATERAL (
    SELECT name, language_code AS lang
    FROM content.tag_translations
    WHERE tag_id = t.id
    LIMIT 1
) tn_any ON true
LEFT JOIN daily    d ON d.tag_id = t.id
LEFT JOIN recent_7d r ON r.tag_id = t.id
WHERE t.visibility = 'public'::content.tag_visibility_enum
ORDER BY trend_score_7d DESC NULLS LAST, total_usage DESC
LIMIT  LEAST(p_limit, 100)
OFFSET p_offset;
$$;


ALTER FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) OWNER TO "postgres";


COMMENT ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) IS 'Returns top N public tags ordered by 7-day trend score.
Language resolution: tries p_lang first, falls back to any available translation, then slug.
Row cap: min(p_limit, 100). Default: top 50 tags in "en".';



CREATE OR REPLACE FUNCTION "public"."set_updated_at"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO ''
    AS $$
BEGIN
    NEW.updated_at := now();
    RETURN NEW;
END;
$$;


ALTER FUNCTION "public"."set_updated_at"() OWNER TO "postgres";


COMMENT ON FUNCTION "public"."set_updated_at"() IS 'Shared BEFORE UPDATE trigger function. Sets updated_at = now(). SECURITY DEFINER, search_path locked to empty. Used cross-schema by wallet, billing, and content updated_at triggers.';



CREATE OR REPLACE FUNCTION "public"."trg_block_unsafe_artifact_delete"() RETURNS "trigger"
    LANGUAGE "plpgsql" SECURITY DEFINER
    SET "search_path" TO 'public'
    AS $$
DECLARE
  v_type TEXT;
  v_summary JSONB;
BEGIN
  IF public.fn_lifecycle_is_service_role() THEN
    RETURN OLD;
  END IF;

  v_type := CASE TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME
    WHEN 'lenses.lenses' THEN 'lens'
    WHEN 'lenses.workflows' THEN 'workflow'
    WHEN 'battles.battles' THEN 'battle'
    WHEN 'agents.ai_lensers' THEN 'agent'
    ELSE NULL
  END;

  IF v_type IS NULL THEN
    RETURN OLD;
  END IF;

  v_summary := public.fn_artifact_dependency_summary(v_type, OLD.id);

  IF COALESCE((v_summary ->> 'total')::int, 0) > 0 THEN
    RAISE EXCEPTION 'unsafe_hard_delete_blocked: use lifecycle delete/archive RPC instead'
      USING ERRCODE = '55000',
            DETAIL = v_summary::text;
  END IF;

  RETURN OLD;
END;
$$;


ALTER FUNCTION "public"."trg_block_unsafe_artifact_delete"() OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."contact_messages" WITH ("security_invoker"='on') AS
 SELECT "id",
    "lenser_id",
    "name",
    "email",
    "subject",
    "message",
    "kvkk_approved",
    "created_at",
    "ip_address",
    "user_agent"
   FROM "analytics"."contact";


ALTER VIEW "public"."contact_messages" OWNER TO "postgres";

SET default_tablespace = '';

SET default_table_access_method = "heap";


CREATE TABLE IF NOT EXISTS "public"."notification_aggregates" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "recipient_id" "uuid" NOT NULL,
    "notification_type" "text" NOT NULL,
    "entity_id" "uuid" NOT NULL,
    "window_start" timestamp with time zone DEFAULT "now"() NOT NULL,
    "window_end" timestamp with time zone NOT NULL,
    "actor_ids" "uuid"[] DEFAULT '{}'::"uuid"[] NOT NULL,
    "notification_id" "uuid",
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "public"."notification_aggregates" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."notification_preferences" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "notification_type" "text" NOT NULL,
    "enabled" boolean DEFAULT true NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL
);


ALTER TABLE "public"."notification_preferences" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."notifications" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "type" "text" NOT NULL,
    "title" "text" NOT NULL,
    "body" "text",
    "action_url" "text",
    "metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "read_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "notifications_type_check" CHECK (("type" = ANY (ARRAY['battle_result'::"text", 'battle_started'::"text", 'vote_reminder'::"text", 'template_battle_open'::"text", 'template_battle_published'::"text", 'vote_received'::"text", 'battle_assigned'::"text", 'battle_vote_cast'::"text", 'battle_joined'::"text", 'battle_won'::"text", 'battle_lost'::"text", 'battle_comment'::"text", 'follow_new'::"text", 'follow_request'::"text", 'follow_accepted'::"text", 'lens_reaction'::"text", 'lens_comment'::"text", 'lens_published'::"text", 'lens_forked'::"text", 'lens_featured'::"text", 'lens_milestone'::"text", 'workflow_published'::"text", 'workflow_forked'::"text", 'agent_update'::"text", 'agent_cron_result'::"text", 'agent_critical'::"text", 'agent_created'::"text", 'agent_battle_won'::"text", 'team_run_started'::"text", 'team_run_completed'::"text", 'team_run_failed'::"text", 'cron_run_completed'::"text", 'cron_run_failed'::"text", 'policy_updated'::"text", 'model_binding_changed'::"text", 'requirement_update'::"text", 'badge_awarded'::"text", 'leaderboard_change'::"text", 'system'::"text"])))
);


ALTER TABLE "public"."notifications" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."user_oauth_connections" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "workspace_id" "uuid" NOT NULL,
    "provider" "text" NOT NULL,
    "capability" "text" NOT NULL,
    "connection_label" "text" NOT NULL,
    "ref" "text" NOT NULL,
    "auth_strategy" "text" DEFAULT 'oauth2'::"text" NOT NULL,
    "provider_config" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
    "access_token_id" "uuid" NOT NULL,
    "refresh_token_id" "uuid",
    "granted_scopes" "text"[] DEFAULT '{}'::"text"[] NOT NULL,
    "expires_at" timestamp with time zone,
    "is_active" boolean DEFAULT true NOT NULL,
    "revoked_at" timestamp with time zone,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "uoc_auth_strategy_check" CHECK (("auth_strategy" = ANY (ARRAY['oauth2'::"text", 'api_key'::"text", 'webhook'::"text", 'none'::"text"]))),
    CONSTRAINT "uoc_capability_check" CHECK (("capability" = ANY (ARRAY['database'::"text", 'page'::"text", 'gmail'::"text", 'drive'::"text", 'sheets'::"text", 'docs'::"text", 'calendar'::"text", 'tasks'::"text", 'boards'::"text", 'webhooks'::"text", 'chat'::"text", 'repos'::"text", 'issues'::"text", 'projects'::"text", 'lists'::"text", 'records'::"text", 'crm'::"text", 'messages'::"text", 'channels'::"text", 'files'::"text", 'events'::"text", 'http'::"text"]))),
    CONSTRAINT "uoc_label_format" CHECK (("connection_label" ~ '^[a-z0-9][a-z0-9_-]{0,47}$'::"text")),
    CONSTRAINT "uoc_provider_check" CHECK (("provider" = ANY (ARRAY['notion'::"text", 'google'::"text", 'asana'::"text", 'monday'::"text", 'zapier'::"text", 'slack'::"text", 'github'::"text", 'gitlab'::"text", 'jira'::"text", 'linear'::"text", 'trello'::"text", 'airtable'::"text", 'hubspot'::"text", 'salesforce'::"text", 'discord'::"text", 'microsoft_teams'::"text", 'microsoft_outlook'::"text", 'microsoft_onedrive'::"text", 'microsoft_excel'::"text", 'dropbox'::"text", 'box'::"text", 'calendly'::"text", 'clickup'::"text", 'todoist'::"text", 'custom_http'::"text"]))),
    CONSTRAINT "uoc_ref_format" CHECK ((("ref" ~ '^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z0-9][a-z0-9_-]{0,47}$'::"text") AND ("char_length"("ref") <= 120))),
    CONSTRAINT "uoc_revoke_consistency" CHECK (((("is_active" = true) AND ("revoked_at" IS NULL)) OR (("is_active" = false) AND ("revoked_at" IS NOT NULL))))
);


ALTER TABLE "public"."user_oauth_connections" OWNER TO "postgres";


COMMENT ON TABLE "public"."user_oauth_connections" IS 'Per-user OAuth connections to external providers. Tokens stored in Supabase Vault (never in this table). The ref column is the stable identifier for [[:connector:ref]] expression syntax. All writes go through SECURITY DEFINER RPCs; direct INSERT/UPDATE/DELETE is revoked from the authenticated role.';



COMMENT ON COLUMN "public"."user_oauth_connections"."ref" IS 'Stable connector reference slug, e.g. ''google.gmail.primary''. Used as the key in [[:connector:google.gmail.primary]] expressions.';



COMMENT ON COLUMN "public"."user_oauth_connections"."access_token_id" IS 'vault.secrets.id for the OAuth access token. Decrypted only server-side.';



COMMENT ON COLUMN "public"."user_oauth_connections"."refresh_token_id" IS 'vault.secrets.id for the OAuth refresh token. Null if provider does not issue one.';



CREATE OR REPLACE VIEW "public"."v_workflow_run_cost_breakdown" WITH ("security_invoker"='on') AS
 SELECT "r"."id" AS "run_id",
    "r"."workflow_id",
    "r"."status" AS "run_status",
    "r"."triggered_by",
    "count"("nr"."id") AS "total_nodes",
    "count"("nr"."id") FILTER (WHERE ("nr"."status" = 'completed'::"text")) AS "completed_nodes",
    "count"("nr"."id") FILTER (WHERE ("nr"."status" = ANY (ARRAY['failed'::"text", 'timed_out'::"text", 'invalidated'::"text", 'blocked'::"text"]))) AS "failed_nodes",
    "count"("nr"."id") FILTER (WHERE ("nr"."status" = 'skipped'::"text")) AS "skipped_nodes",
    COALESCE("sum"("nr"."input_tokens"), (0)::bigint) AS "total_input_tokens",
    COALESCE("sum"("nr"."output_tokens"), (0)::bigint) AS "total_output_tokens",
    COALESCE("sum"("nr"."cost_credits"), (0)::bigint) AS "total_cost_credits",
    COALESCE("sum"("nr"."retry_count"), (0)::bigint) AS "total_retries",
    (COALESCE("avg"("nr"."duration_ms"), (0)::numeric))::integer AS "avg_duration_ms",
    COALESCE("max"("nr"."duration_ms"), 0) AS "max_duration_ms",
    (COALESCE("avg"("nr"."ttfb_ms"), (0)::numeric))::integer AS "avg_ttfb_ms"
   FROM ("lenses"."workflow_runs" "r"
     LEFT JOIN "lenses"."workflow_node_results" "nr" ON (("nr"."run_id" = "r"."id")))
  GROUP BY "r"."id", "r"."workflow_id", "r"."status", "r"."triggered_by";


ALTER VIEW "public"."v_workflow_run_cost_breakdown" OWNER TO "postgres";


COMMENT ON VIEW "public"."v_workflow_run_cost_breakdown" IS 'Phase 6 — Per-run token + credit + latency rollup. Drives cost dashboards and per-lenser usage reports.';



CREATE OR REPLACE VIEW "public"."v_workflow_run_health" WITH ("security_invoker"='on') AS
 SELECT "id" AS "run_id",
    "workflow_id",
    "status",
    "run_worker_id",
    "heartbeat_at",
    "started_at",
    "completed_at",
    "parent_run_id",
    "recursion_depth",
    EXTRACT(epoch FROM ("now"() - COALESCE("heartbeat_at", "started_at"))) AS "seconds_since_heartbeat",
        CASE
            WHEN ("status" = ANY (ARRAY['completed'::"text", 'failed'::"text", 'cancelled'::"text", 'timed_out'::"text"])) THEN 'terminal'::"text"
            WHEN ("heartbeat_at" IS NULL) THEN 'no_heartbeat'::"text"
            WHEN (("now"() - "heartbeat_at") > '00:01:00'::interval) THEN 'stale'::"text"
            ELSE 'live'::"text"
        END AS "liveness",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_node_results" "nr"
          WHERE (("nr"."run_id" = "r"."id") AND ("nr"."status" = ANY (ARRAY['pending'::"text", 'awaiting_dependency'::"text", 'queued'::"text"])))) AS "pending_nodes",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_node_results" "nr"
          WHERE (("nr"."run_id" = "r"."id") AND ("nr"."status" = ANY (ARRAY['running'::"text", 'streaming'::"text", 'retrying'::"text"])))) AS "in_flight_nodes",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_node_results" "nr"
          WHERE (("nr"."run_id" = "r"."id") AND ("nr"."status" = ANY (ARRAY['completed'::"text", 'skipped'::"text"])))) AS "done_nodes"
   FROM "lenses"."workflow_runs" "r";


ALTER VIEW "public"."v_workflow_run_health" OWNER TO "postgres";


COMMENT ON VIEW "public"."v_workflow_run_health" IS 'Phase 6 + 7 — Liveness + progress signals per run. `liveness` maps to recovery actions: live = leave alone, stale = candidate for fn_claim_stale_workflow_run, no_heartbeat = never advanced past queued, terminal = finished.';



CREATE OR REPLACE VIEW "public"."v_workflow_run_timeline" WITH ("security_invoker"='on') AS
 SELECT "r"."id" AS "run_id",
    "r"."workflow_id",
    "r"."status" AS "run_status",
    "r"."started_at" AS "run_started_at",
    "nr"."node_id",
    "n"."label" AS "node_label",
    "nr"."status" AS "node_status",
    "nr"."started_at" AS "node_started_at",
    "nr"."completed_at" AS "node_completed_at",
    "nr"."retry_count",
    "nr"."duration_ms",
    "nr"."ttfb_ms",
    "nr"."error_message",
    EXTRACT(epoch FROM ("nr"."started_at" - "r"."started_at")) AS "rel_start_s",
    EXTRACT(epoch FROM ("nr"."completed_at" - "r"."started_at")) AS "rel_end_s"
   FROM (("lenses"."workflow_runs" "r"
     JOIN "lenses"."workflow_node_results" "nr" ON (("nr"."run_id" = "r"."id")))
     LEFT JOIN "lenses"."workflow_nodes" "n" ON (("n"."id" = "nr"."node_id")))
  ORDER BY "r"."id", "nr"."started_at", "nr"."node_id";


ALTER VIEW "public"."v_workflow_run_timeline" OWNER TO "postgres";


COMMENT ON VIEW "public"."v_workflow_run_timeline" IS 'Phase 6 — Chronological timeline of node execution within a run. Used by the monitoring UI to render Gantt-style lanes.';



CREATE OR REPLACE VIEW "public"."vw_ai_models_public" AS
 SELECT "m"."id",
    "m"."name",
    "m"."key",
    "p"."key" AS "provider_key",
    "p"."display_name" AS "provider_name",
    "m"."context_window_tokens",
    "m"."supports_tools",
    "m"."supports_json_schema",
    "m"."supports_vision",
    "m"."capabilities",
    "m"."input_modalities",
    "m"."output_modalities",
    "m"."is_active"
   FROM ("ai"."models" "m"
     JOIN "ai"."providers" "p" ON (("p"."id" = "m"."provider_id")))
  WHERE (("m"."is_active" = true) AND ("p"."is_active" = true))
  ORDER BY "p"."display_name", "m"."name";


ALTER VIEW "public"."vw_ai_models_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_auth_lenser" WITH ("security_invoker"='on') AS
 SELECT "u"."id" AS "user_id",
    "l"."id" AS "lenser_id"
   FROM ("auth"."users" "u"
     JOIN "lensers"."profiles" "l" ON (("l"."user_id" = "u"."id")));


ALTER VIEW "public"."vw_auth_lenser" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battle_funnel" WITH ("security_invoker"='on') AS
 WITH "status_counts" AS (
         SELECT "count"(*) AS "total",
            "count"(*) FILTER (WHERE ("battles"."status" <> 'draft'::"battles"."battle_status_enum")) AS "past_draft",
            "count"(*) FILTER (WHERE ("battles"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"]))) AS "reached_voting",
            "count"(*) FILTER (WHERE ("battles"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"]))) AS "reached_closed",
            "count"(*) FILTER (WHERE ("battles"."status" = ANY (ARRAY['published'::"battles"."battle_status_enum", 'archived'::"battles"."battle_status_enum"]))) AS "reached_published",
            "count"(*) FILTER (WHERE ("battles"."status" = 'archived'::"battles"."battle_status_enum")) AS "reached_archived"
           FROM "battles"."battles"
          WHERE ("battles"."deleted_at" IS NULL)
        )
 SELECT "total",
    "past_draft",
    "reached_voting",
    "reached_closed",
    "reached_published",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("past_draft")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_past_draft",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("reached_voting")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_reached_voting",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("reached_closed")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_reached_closed",
        CASE
            WHEN ("total" > 0) THEN "round"(((100.0 * ("reached_published")::numeric) / ("total")::numeric), 1)
            ELSE (0)::numeric
        END AS "pct_reached_published"
   FROM "status_counts";


ALTER VIEW "public"."vw_battle_funnel" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battle_health" WITH ("security_invoker"='on') AS
 SELECT "b"."id",
    "b"."title",
    "b"."slug",
    "b"."status",
    "b"."created_at",
    "b"."voting_opens_at",
    "b"."voting_closes_at",
    "b"."finalized_at",
    "b"."published_at",
    "b"."total_vote_count",
        CASE
            WHEN ("b"."total_vote_count" >= 5) THEN 'confident'::"text"
            WHEN ("b"."total_vote_count" >= 3) THEN 'moderate'::"text"
            ELSE 'low'::"text"
        END AS "confidence_level",
    "b"."winner_contender_id",
    "cc"."contender_count",
    "sc"."submission_count",
        CASE
            WHEN (("b"."finalized_at" IS NOT NULL) AND ("b"."created_at" IS NOT NULL)) THEN (EXTRACT(epoch FROM ("b"."finalized_at" - "b"."created_at")) / 3600.0)
            ELSE NULL::numeric
        END AS "hours_to_finalize"
   FROM (("battles"."battles" "b"
     LEFT JOIN LATERAL ( SELECT "count"(*) AS "contender_count"
           FROM "battles"."contenders" "c"
          WHERE ("c"."battle_id" = "b"."id")) "cc" ON (true))
     LEFT JOIN LATERAL ( SELECT "count"(*) AS "submission_count"
           FROM "battles"."submissions" "s"
          WHERE (("s"."battle_id" = "b"."id") AND ("s"."status" = 'submitted'::"battles"."submission_status_enum"))) "sc" ON (true))
  WHERE ("b"."deleted_at" IS NULL);


ALTER VIEW "public"."vw_battle_health" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battle_participation" AS
 WITH "weekly_battles" AS (
         SELECT "date_trunc"('week'::"text", "b"."created_at") AS "week",
            "count"(DISTINCT "b"."id") AS "battles_created",
            "count"(DISTINCT
                CASE
                    WHEN ("b"."status" = ANY (ARRAY['closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) THEN "b"."id"
                    ELSE NULL::"uuid"
                END) AS "battles_completed",
            "count"(DISTINCT "b"."creator_lenser_id") AS "unique_hosts"
           FROM "battles"."battles" "b"
          WHERE ("b"."deleted_at" IS NULL)
          GROUP BY ("date_trunc"('week'::"text", "b"."created_at"))
        ), "weekly_votes" AS (
         SELECT "date_trunc"('week'::"text", "b"."created_at") AS "week",
            "count"(DISTINCT "v"."voter_lenser_id") AS "unique_voters",
            "count"(*) AS "total_votes"
           FROM ("battles"."votes" "v"
             JOIN "battles"."battles" "b" ON (("b"."id" = "v"."battle_id")))
          WHERE ("b"."deleted_at" IS NULL)
          GROUP BY ("date_trunc"('week'::"text", "b"."created_at"))
        )
 SELECT "wb"."week",
    "wb"."battles_created",
    "wb"."battles_completed",
    "wb"."unique_hosts",
    COALESCE("wv"."unique_voters", (0)::bigint) AS "unique_voters",
    COALESCE("wv"."total_votes", (0)::bigint) AS "total_votes"
   FROM ("weekly_battles" "wb"
     LEFT JOIN "weekly_votes" "wv" ON (("wv"."week" = "wb"."week")))
  ORDER BY "wb"."week" DESC;


ALTER VIEW "public"."vw_battle_participation" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_battles_public" AS
 SELECT "b"."id",
    "b"."title",
    "b"."slug",
    "b"."status",
    "b"."creator_lenser_id",
    "b"."total_vote_count",
    "b"."created_at",
    "b"."updated_at",
    "cc"."contender_count"
   FROM ("battles"."battles" "b"
     LEFT JOIN LATERAL ( SELECT "count"(*) AS "contender_count"
           FROM "battles"."contenders" "c"
          WHERE ("c"."battle_id" = "b"."id")) "cc" ON (true))
  WHERE (("b"."status" = ANY (ARRAY['voting'::"battles"."battle_status_enum", 'scoring'::"battles"."battle_status_enum", 'closed'::"battles"."battle_status_enum", 'published'::"battles"."battle_status_enum"])) AND ("b"."deleted_at" IS NULL));


ALTER VIEW "public"."vw_battles_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_content_tags_public" AS
 SELECT "t"."id",
    "t"."slug",
    COALESCE("tn"."name", "t"."slug") AS "name",
    "t"."visibility"
   FROM ("content"."tags" "t"
     LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
           FROM "content"."tag_translations"
          WHERE ("tag_translations"."tag_id" = "t"."id")
         LIMIT 1) "tn" ON (true))
  WHERE ("t"."visibility" = 'public'::"content"."tag_visibility_enum");


ALTER VIEW "public"."vw_content_tags_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_content_thread_replies_public" AS
 SELECT "r"."id",
    "r"."thread_id",
    "r"."parent_reply_id",
    "r"."lenser_id",
    "r"."content",
    "r"."content_html",
    "rt"."reaction_totals",
    "r"."created_at",
    "jsonb_build_object"('handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile"
   FROM ((("content"."thread_replies" "r"
     JOIN "content"."threads" "t" ON (("t"."id" = "r"."thread_id")))
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "r"."lenser_id")))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals"
           FROM ( SELECT "rx"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "rx"
                  WHERE (("rx"."entity_type" = 'thread_reply'::"content"."entity_type_enum") AND ("rx"."entity_id" = "r"."id"))
                  GROUP BY "rx"."reaction") "x") "rt" ON (true))
  WHERE (("t"."visibility" = 'public'::"content"."visibility_enum") AND ("t"."status" = 'published'::"content"."content_status") AND ("r"."status" = 'published'::"content"."thread_reply_status") AND ("r"."deleted_at" IS NULL))
  ORDER BY "r"."created_at";


ALTER VIEW "public"."vw_content_thread_replies_public" OWNER TO "postgres";


COMMENT ON VIEW "public"."vw_content_thread_replies_public" IS 'Public view of published thread replies with reaction aggregates. Uses unified content.reactions table (updated in 20260440000012).';



CREATE OR REPLACE VIEW "public"."vw_content_threads_public" AS
 SELECT "t"."id",
    "t"."lenser_id",
    "prof"."handle" AS "lenser_handle",
    COALESCE("et"."title", 'Untitled'::"text") AS "title",
    COALESCE("et"."content", ''::"text") AS "content",
    "jsonb_build_object"('handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile",
    "rt"."reaction_totals",
    "rt"."like_count",
    "t"."reply_count",
    "t"."view_count",
    "t"."created_at",
    "t"."thumbnail_url",
    "t"."lens_data",
    "t"."visibility",
    "tg_agg"."tags"
   FROM (((("content"."threads" "t"
     LEFT JOIN "content"."entity_translations" "et" ON ((("et"."entity_id" = "t"."id") AND ("et"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("et"."is_original" = true))))
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "t"."lenser_id")))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'like'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "like_count"
           FROM ( SELECT "rx"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "rx"
                  WHERE (("rx"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("rx"."entity_id" = "t"."id"))
                  GROUP BY "rx"."reaction") "x") "rt" ON (true))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_agg"("jsonb_build_object"('id', "tg"."id", 'slug', "tg"."slug", 'name', COALESCE("tn"."name", "tg"."slug"))), '[]'::"jsonb") AS "tags"
           FROM (("content"."tag_map" "tm"
             JOIN "content"."tags" "tg" ON (("tg"."id" = "tm"."tag_id")))
             LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
                   FROM "content"."tag_translations"
                  WHERE ("tag_translations"."tag_id" = "tg"."id")
                 LIMIT 1) "tn" ON (true))
          WHERE (("tm"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("tm"."entity_id" = "t"."id"))) "tg_agg" ON (true))
  WHERE (("t"."visibility" = 'public'::"content"."visibility_enum") AND ("t"."status" = 'published'::"content"."content_status"));


ALTER VIEW "public"."vw_content_threads_public" OWNER TO "postgres";


COMMENT ON VIEW "public"."vw_content_threads_public" IS 'Public view of published threads with reaction aggregates. Uses unified content.reactions and content.entity_translations tables (updated in 20260440000012).';



COMMENT ON COLUMN "public"."vw_content_threads_public"."lenser_handle" IS 'Flat text copy of the author handle. Use eq(lenser_handle, ''foo'') for profile-page filtering — this is indexable, unlike author_profile->>(''handle'').';



CREATE OR REPLACE VIEW "public"."vw_feedback_admin" WITH ("security_invoker"='on') AS
 SELECT "id",
    "product_tag",
    "page",
    "user_id",
    "message",
    "start_date",
    "end_date",
    "created_at",
    "status"
   FROM "analytics"."product_feedback";


ALTER VIEW "public"."vw_feedback_admin" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_feedback_user" AS
 SELECT "product_tag",
    "page",
    "message",
    "start_date",
    "end_date",
    "status",
    "created_at"
   FROM "analytics"."product_feedback"
  WHERE ("user_id" = "auth"."uid"());


ALTER VIEW "public"."vw_feedback_user" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_global_messages" AS
 SELECT "id",
    "battle_id",
    "sender_id",
    "sender_handle",
    "sender_role",
    "body",
    "created_at"
   FROM "battles"."global_messages";


ALTER VIEW "public"."vw_global_messages" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lensers_public_recent" AS
 SELECT "l"."handle",
    "l"."display_name",
    "l"."avatar_url",
    "l"."headline",
    "l"."status",
    "l"."created_at",
    "t"."lenser_id",
    "t"."total_xp",
    "t"."current_level",
    "t"."app_id",
    "lv"."min_total_xp" AS "min_xp",
    "lv"."max_total_xp" AS "max_xp",
    "jl"."join_order",
    "jl"."joined_at",
    "s"."thread_count",
    "s"."lens_count",
    "s"."follower_count",
    "s"."following_count"
   FROM (((("lensers"."profiles" "l"
     LEFT JOIN "xp"."totals" "t" ON (("t"."lenser_id" = "l"."id")))
     LEFT JOIN "xp"."levels" "lv" ON ((("lv"."app_id" = "t"."app_id") AND ("lv"."level" = "t"."current_level"))))
     LEFT JOIN "analytics"."lenser_join_log" "jl" ON (("jl"."lenser_id" = "l"."id")))
     LEFT JOIN "analytics"."lenser_stats" "s" ON (("s"."lenser_id" = "l"."id")))
  WHERE (("l"."status" = 'active'::"lensers"."lenser_status") AND ("l"."deletion_requested_at" IS NULL))
  ORDER BY "l"."created_at" DESC
 LIMIT 5;


ALTER VIEW "public"."vw_lensers_public_recent" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lensers_social_links_private" WITH ("security_invoker"='on') AS
 SELECT "p"."handle",
    "l"."id",
    "l"."platform",
    "l"."url",
    "l"."label",
    "l"."created_at"
   FROM ("lensers"."profiles" "p"
     JOIN "lensers"."social_links" "l" ON (("l"."lenser_id" = "p"."id")))
  WHERE ("p"."user_id" = "auth"."uid"());


ALTER VIEW "public"."vw_lensers_social_links_private" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lensers_social_links_public" AS
 SELECT "p"."handle",
    "l"."platform",
    "l"."url",
    "l"."label"
   FROM ("lensers"."profiles" "p"
     JOIN "lensers"."social_links" "l" ON (("l"."lenser_id" = "p"."id")))
  WHERE (("p"."status" = 'active'::"lensers"."lenser_status") AND ("p"."deletion_requested_at" IS NULL) AND ("p"."visibility" = 'public'::"lensers"."lenser_visibility"));


ALTER VIEW "public"."vw_lensers_social_links_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_lenses_public" AS
 SELECT "pt"."id",
    "pt"."lenser_id",
    "prof"."handle" AS "lenser_handle",
    "pt"."visibility",
    COALESCE("et"."title", 'Untitled'::"text") AS "title",
    "et"."description",
    COALESCE("et"."content", ''::"text") AS "content",
    "jsonb_build_object"('id', "prof"."id", 'handle', "prof"."handle", 'display_name', "prof"."display_name", 'avatar_url', "prof"."avatar_url") AS "author_profile",
    "rt"."reaction_totals",
    "rt"."copy_count",
    "rt"."like_count",
    "rt"."saved_count",
    "pt"."created_at",
    "tg_agg"."tags"
   FROM (((("lenses"."lenses" "pt"
     LEFT JOIN "content"."entity_translations" "et" ON ((("et"."entity_id" = "pt"."id") AND ("et"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("et"."is_original" = true))))
     LEFT JOIN "lensers"."profiles" "prof" ON (("prof"."id" = "pt"."lenser_id")))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_object_agg"("x"."reaction", "x"."cnt"), '{}'::"jsonb") AS "reaction_totals",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'copy'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "copy_count",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'like'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "like_count",
            COALESCE(("sum"(
                CASE
                    WHEN ("x"."reaction" = 'saved'::"content"."reaction_enum") THEN "x"."cnt"
                    ELSE 0
                END))::integer, 0) AS "saved_count"
           FROM ( SELECT "rx"."reaction",
                    ("count"(*))::integer AS "cnt"
                   FROM "content"."reactions" "rx"
                  WHERE (("rx"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("rx"."entity_id" = "pt"."id"))
                  GROUP BY "rx"."reaction") "x") "rt" ON (true))
     LEFT JOIN LATERAL ( SELECT COALESCE("jsonb_agg"("jsonb_build_object"('id', "tg"."id", 'slug', "tg"."slug", 'name', COALESCE("tn"."name", "tg"."slug"))), '[]'::"jsonb") AS "tags"
           FROM (("content"."tag_map" "tm"
             JOIN "content"."tags" "tg" ON (("tg"."id" = "tm"."tag_id")))
             LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
                   FROM "content"."tag_translations"
                  WHERE ("tag_translations"."tag_id" = "tg"."id")
                 LIMIT 1) "tn" ON (true))
          WHERE (("tm"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("tm"."entity_id" = "pt"."id"))) "tg_agg" ON (true))
  WHERE (("pt"."visibility" = 'public'::"content"."visibility_enum") AND ("pt"."status" = 'published'::"content"."content_status"));


ALTER VIEW "public"."vw_lenses_public" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_tags_public_extended" AS
 SELECT "t"."id",
    "t"."slug",
    COALESCE("tn"."name", "t"."slug") AS "name",
    "t"."created_at",
    'public'::"text" AS "visibility",
    (0)::bigint AS "created_count",
    (0)::bigint AS "viewed_count",
    (0)::bigint AS "reacted_count",
    (0)::bigint AS "total_usage",
    (0)::bigint AS "trend_score"
   FROM ("content"."tags" "t"
     LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
           FROM "content"."tag_translations"
          WHERE ("tag_translations"."tag_id" = "t"."id")
         LIMIT 1) "tn" ON (true))
  WHERE ("t"."visibility" = 'public'::"content"."tag_visibility_enum");


ALTER VIEW "public"."vw_tags_public_extended" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_tags_public_stats" AS
 WITH "events_filtered" AS (
         SELECT "e"."tag_id",
            "e"."activity_type",
            ("e"."occurred_at")::"date" AS "activity_date"
           FROM ((("analytics"."tag_activity_events" "e"
             JOIN "content"."tags" "t_1" ON ((("t_1"."id" = "e"."tag_id") AND ("t_1"."visibility" = 'public'::"content"."tag_visibility_enum"))))
             LEFT JOIN "lenses"."lenses" "p" ON ((("e"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("e"."entity_id" = "p"."id"))))
             LEFT JOIN "content"."threads" "th" ON ((("e"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("e"."entity_id" = "th"."id"))))
          WHERE ((("e"."entity_type" = 'lens'::"content"."entity_type_enum") AND ("p"."visibility" = 'public'::"content"."visibility_enum")) OR (("e"."entity_type" = 'thread'::"content"."entity_type_enum") AND ("th"."visibility" = 'public'::"content"."visibility_enum")))
        ), "lifetime" AS (
         SELECT "events_filtered"."tag_id",
            "count"(*) FILTER (WHERE ("events_filtered"."activity_type" = 'created'::"text")) AS "created_count",
            "count"(*) FILTER (WHERE ("events_filtered"."activity_type" = 'viewed'::"text")) AS "viewed_count",
            "count"(*) FILTER (WHERE ("events_filtered"."activity_type" = 'reacted'::"text")) AS "reacted_count"
           FROM "events_filtered"
          GROUP BY "events_filtered"."tag_id"
        ), "recent_7d" AS (
         SELECT "events_filtered"."tag_id",
            "sum"(((
                CASE
                    WHEN ("events_filtered"."activity_type" = 'created'::"text") THEN 1
                    ELSE 0
                END +
                CASE
                    WHEN ("events_filtered"."activity_type" = 'viewed'::"text") THEN 2
                    ELSE 0
                END) +
                CASE
                    WHEN ("events_filtered"."activity_type" = 'reacted'::"text") THEN 3
                    ELSE 0
                END)) AS "trend_score_7d"
           FROM "events_filtered"
          WHERE ("events_filtered"."activity_date" >= (CURRENT_DATE - '7 days'::interval))
          GROUP BY "events_filtered"."tag_id"
        )
 SELECT "t"."id",
    "t"."slug",
    COALESCE("tn"."name", "t"."slug") AS "name",
    "t"."created_at",
    'public'::"text" AS "visibility",
    COALESCE("l"."created_count", (0)::bigint) AS "created_count",
    COALESCE("l"."viewed_count", (0)::bigint) AS "viewed_count",
    COALESCE("l"."reacted_count", (0)::bigint) AS "reacted_count",
    ((COALESCE("l"."created_count", (0)::bigint) + COALESCE("l"."viewed_count", (0)::bigint)) + COALESCE("l"."reacted_count", (0)::bigint)) AS "total_usage",
    COALESCE("r"."trend_score_7d", (0)::bigint) AS "trend_score_7d"
   FROM ((("content"."tags" "t"
     LEFT JOIN LATERAL ( SELECT "tag_translations"."name"
           FROM "content"."tag_translations"
          WHERE ("tag_translations"."tag_id" = "t"."id")
         LIMIT 1) "tn" ON (true))
     LEFT JOIN "lifetime" "l" ON (("l"."tag_id" = "t"."id")))
     LEFT JOIN "recent_7d" "r" ON (("r"."tag_id" = "t"."id")))
  WHERE ("t"."visibility" = 'public'::"content"."tag_visibility_enum");


ALTER VIEW "public"."vw_tags_public_stats" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_workflows" AS
 SELECT "id",
    "lenser_id",
    "title",
    "description",
    "visibility",
    "battle_count",
    ( SELECT "count"(*) AS "count"
           FROM "lenses"."workflow_nodes" "wn"
          WHERE ("wn"."workflow_id" = "w"."id")) AS "node_count",
    "reaction_totals",
    "fork_count",
    "parent_workflow_id",
    "created_at",
    "updated_at"
   FROM "lenses"."workflows" "w";


ALTER VIEW "public"."vw_workflows" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_xp_leaderboard_global" AS
 WITH "aggregated" AS (
         SELECT "t"."lenser_id",
            "sum"("t"."total_xp") AS "total_xp",
            "max"("t"."current_level") AS "current_level"
           FROM "xp"."totals" "t"
          GROUP BY "t"."lenser_id"
        ), "ranked" AS (
         SELECT "a"."lenser_id",
            "a"."total_xp",
            "a"."current_level",
            "rank"() OVER (ORDER BY "a"."total_xp" DESC, "a"."lenser_id") AS "rank"
           FROM "aggregated" "a"
        )
 SELECT "r"."rank",
    "r"."lenser_id",
    "r"."total_xp",
    "r"."current_level",
    "jsonb_build_object"('display_name', "l"."display_name", 'handle', "l"."handle", 'avatar_url', "l"."avatar_url") AS "user"
   FROM ("ranked" "r"
     JOIN "lensers"."profiles" "l" ON (("l"."id" = "r"."lenser_id")))
  WHERE ("r"."rank" <= 100);


ALTER VIEW "public"."vw_xp_leaderboard_global" OWNER TO "postgres";


CREATE OR REPLACE VIEW "public"."vw_xp_leaderboard_season" AS
 WITH "ranked" AS (
         SELECT "st"."season_id",
            "s"."slug" AS "season_slug",
            "st"."app_id",
            "st"."lenser_id",
            "st"."total_xp",
            "rank"() OVER (PARTITION BY "st"."season_id", "st"."app_id" ORDER BY "st"."total_xp" DESC, "st"."lenser_id") AS "rank"
           FROM ("xp"."season_totals" "st"
             JOIN "xp"."seasons" "s" ON (("s"."id" = "st"."season_id")))
        ), "top100" AS (
         SELECT "ranked"."season_id",
            "ranked"."season_slug",
            "ranked"."app_id",
            "ranked"."lenser_id",
            "ranked"."total_xp",
            "ranked"."rank"
           FROM "ranked"
          WHERE ("ranked"."rank" <= 100)
        ), "me" AS (
         SELECT "ranked"."season_id",
            "ranked"."season_slug",
            "ranked"."app_id",
            "ranked"."lenser_id",
            "ranked"."total_xp",
            "ranked"."rank"
           FROM "ranked"
          WHERE ("ranked"."lenser_id" = "auth"."uid"())
        )
 SELECT "t"."season_id",
    "t"."season_slug",
    "t"."app_id",
    "t"."rank",
    "t"."lenser_id",
    "t"."total_xp",
    "jsonb_build_object"('display_name', "l"."display_name", 'handle', "l"."handle", 'avatar_url', "l"."avatar_url") AS "user"
   FROM ("top100" "t"
     JOIN "lensers"."profiles" "l" ON (("l"."id" = "t"."lenser_id")))
UNION
 SELECT "m"."season_id",
    "m"."season_slug",
    "m"."app_id",
    "m"."rank",
    "m"."lenser_id",
    "m"."total_xp",
    "jsonb_build_object"('display_name', "l"."display_name", 'handle', "l"."handle", 'avatar_url', "l"."avatar_url") AS "user"
   FROM ("me" "m"
     JOIN "lensers"."profiles" "l" ON (("l"."id" = "m"."lenser_id")));


ALTER VIEW "public"."vw_xp_leaderboard_season" OWNER TO "postgres";


CREATE TABLE IF NOT EXISTS "public"."workflow_integration_credentials" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "lenser_id" "uuid" NOT NULL,
    "integration_type" "text" NOT NULL,
    "label" "text",
    "encrypted_config" "bytea" NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "chk_wic_label_length" CHECK ((("label" IS NULL) OR ("length"("label") <= 100))),
    CONSTRAINT "chk_wic_type_valid" CHECK (("integration_type" = ANY (ARRAY['email'::"text", 'slack'::"text", 'discord'::"text", 'github'::"text", 'notion'::"text", 'google_sheets'::"text"])))
);


ALTER TABLE "public"."workflow_integration_credentials" OWNER TO "postgres";


COMMENT ON TABLE "public"."workflow_integration_credentials" IS 'AES-GCM encrypted credentials for third-party integrations. Decrypt only via SECURITY DEFINER fn.';



CREATE TABLE IF NOT EXISTS "public"."workflow_kv_store" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "run_id" "uuid",
    "key" "text" NOT NULL,
    "value" "jsonb",
    "expires_at" timestamp with time zone DEFAULT ("now"() + '24:00:00'::interval) NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "chk_kv_key_format" CHECK (("key" ~ '^[\w\-\.]+$'::"text")),
    CONSTRAINT "chk_kv_key_length" CHECK (("length"("key") <= 256))
);


ALTER TABLE "public"."workflow_kv_store" OWNER TO "postgres";


COMMENT ON TABLE "public"."workflow_kv_store" IS 'Ephemeral key-value state for workflow runs. Scoped by run_id. TTL enforced by pg_cron.';



CREATE TABLE IF NOT EXISTS "public"."workflow_webhook_triggers" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "workflow_id" "uuid" NOT NULL,
    "secret" "text" NOT NULL,
    "is_active" boolean DEFAULT true NOT NULL,
    "created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
    CONSTRAINT "chk_webhook_secret_length" CHECK ((("length"("secret") >= 16) AND ("length"("secret") <= 512)))
);


ALTER TABLE "public"."workflow_webhook_triggers" OWNER TO "postgres";


COMMENT ON TABLE "public"."workflow_webhook_triggers" IS 'Inbound webhook endpoints that trigger workflow runs on POST.';



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_recipient_id_notification_type_enti_key" UNIQUE ("recipient_id", "notification_type", "entity_id", "window_start");



ALTER TABLE ONLY "public"."notification_preferences"
    ADD CONSTRAINT "notification_preferences_lenser_id_notification_type_key" UNIQUE ("lenser_id", "notification_type");



ALTER TABLE ONLY "public"."notification_preferences"
    ADD CONSTRAINT "notification_preferences_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."notifications"
    ADD CONSTRAINT "notifications_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "uoc_unique_ref_per_lenser" UNIQUE ("lenser_id", "workspace_id", "ref");



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "user_oauth_connections_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."workflow_integration_credentials"
    ADD CONSTRAINT "workflow_integration_credentials_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."workflow_kv_store"
    ADD CONSTRAINT "workflow_kv_store_pkey" PRIMARY KEY ("id");



ALTER TABLE ONLY "public"."workflow_webhook_triggers"
    ADD CONSTRAINT "workflow_webhook_triggers_pkey" PRIMARY KEY ("id");



CREATE INDEX "idx_notif_agg_open" ON "public"."notification_aggregates" USING "btree" ("recipient_id", "notification_type", "entity_id", "window_end");



CREATE INDEX "idx_notif_prefs_lenser" ON "public"."notification_preferences" USING "btree" ("lenser_id");



CREATE INDEX "idx_notifications_lenser_all" ON "public"."notifications" USING "btree" ("lenser_id", "created_at" DESC);



CREATE INDEX "idx_notifications_lenser_unread" ON "public"."notifications" USING "btree" ("lenser_id", "created_at" DESC) WHERE ("read_at" IS NULL);



CREATE INDEX "idx_uoc_lenser_active" ON "public"."user_oauth_connections" USING "btree" ("lenser_id") WHERE (("is_active" = true) AND ("revoked_at" IS NULL));



CREATE INDEX "idx_uoc_lenser_provider" ON "public"."user_oauth_connections" USING "btree" ("lenser_id", "provider") WHERE ("is_active" = true);



CREATE INDEX "idx_uoc_ref_active" ON "public"."user_oauth_connections" USING "btree" ("ref") WHERE (("is_active" = true) AND ("revoked_at" IS NULL));



CREATE INDEX "idx_uoc_workspace_lenser_ref_active" ON "public"."user_oauth_connections" USING "btree" ("workspace_id", "lenser_id", "ref") WHERE (("is_active" = true) AND ("revoked_at" IS NULL));



CREATE INDEX "idx_wic_lenser_type" ON "public"."workflow_integration_credentials" USING "btree" ("lenser_id", "integration_type");



CREATE INDEX "idx_workflow_kv_store_expiry" ON "public"."workflow_kv_store" USING "btree" ("expires_at");



CREATE INDEX "idx_workflow_kv_store_lookup" ON "public"."workflow_kv_store" USING "btree" ("workflow_id", "key");



CREATE UNIQUE INDEX "idx_workflow_webhook_triggers_secret" ON "public"."workflow_webhook_triggers" USING "btree" ("secret");



CREATE INDEX "idx_workflow_webhook_triggers_workflow" ON "public"."workflow_webhook_triggers" USING "btree" ("workflow_id") WHERE ("is_active" = true);



CREATE OR REPLACE TRIGGER "trg_guard_notification_fields" BEFORE UPDATE ON "public"."notifications" FOR EACH ROW EXECUTE FUNCTION "public"."guard_notification_fields"();



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_notification_id_fkey" FOREIGN KEY ("notification_id") REFERENCES "public"."notifications"("id") ON DELETE SET NULL;



ALTER TABLE ONLY "public"."notification_aggregates"
    ADD CONSTRAINT "notification_aggregates_recipient_id_fkey" FOREIGN KEY ("recipient_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."notification_preferences"
    ADD CONSTRAINT "notification_preferences_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."notifications"
    ADD CONSTRAINT "notifications_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "user_oauth_connections_lenser_fk" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."user_oauth_connections"
    ADD CONSTRAINT "user_oauth_connections_workspace_fk" FOREIGN KEY ("workspace_id") REFERENCES "tenancy"."workspaces"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."workflow_integration_credentials"
    ADD CONSTRAINT "workflow_integration_credentials_lenser_id_fkey" FOREIGN KEY ("lenser_id") REFERENCES "lensers"."profiles"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."workflow_kv_store"
    ADD CONSTRAINT "workflow_kv_store_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



ALTER TABLE ONLY "public"."workflow_webhook_triggers"
    ADD CONSTRAINT "workflow_webhook_triggers_workflow_id_fkey" FOREIGN KEY ("workflow_id") REFERENCES "lenses"."workflows"("id") ON DELETE CASCADE;



CREATE POLICY "notif_prefs_owner_insert" ON "public"."notification_preferences" FOR INSERT TO "authenticated" WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notif_prefs_owner_select" ON "public"."notification_preferences" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notif_prefs_owner_update" ON "public"."notification_preferences" FOR UPDATE TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



ALTER TABLE "public"."notification_aggregates" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "notification_aggregates_owner_select" ON "public"."notification_aggregates" FOR SELECT TO "authenticated" USING (("recipient_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notification_aggregates_service_all" ON "public"."notification_aggregates" TO "service_role" USING (true) WITH CHECK (true);



ALTER TABLE "public"."notification_preferences" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "public"."notifications" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "notifications_owner_select" ON "public"."notifications" FOR SELECT TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notifications_owner_update" ON "public"."notifications" FOR UPDATE TO "authenticated" USING (("lenser_id" = "lensers"."get_auth_lenser_id"())) WITH CHECK (("lenser_id" = "lensers"."get_auth_lenser_id"()));



CREATE POLICY "notifications_service_insert" ON "public"."notifications" FOR INSERT TO "service_role" WITH CHECK (true);



CREATE POLICY "uoc_owner_select" ON "public"."user_oauth_connections" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



ALTER TABLE "public"."user_oauth_connections" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "wic_owner_delete" ON "public"."workflow_integration_credentials" FOR DELETE USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



CREATE POLICY "wic_owner_insert" ON "public"."workflow_integration_credentials" FOR INSERT WITH CHECK (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



CREATE POLICY "wic_owner_select" ON "public"."workflow_integration_credentials" FOR SELECT USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



CREATE POLICY "wic_owner_update" ON "public"."workflow_integration_credentials" FOR UPDATE USING (("lenser_id" = ( SELECT "profiles"."id"
   FROM "lensers"."profiles"
  WHERE ("profiles"."user_id" = "auth"."uid"()))));



ALTER TABLE "public"."workflow_integration_credentials" ENABLE ROW LEVEL SECURITY;


ALTER TABLE "public"."workflow_kv_store" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_kv_store_owner_delete" ON "public"."workflow_kv_store" FOR DELETE USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_kv_store_owner_insert" ON "public"."workflow_kv_store" FOR INSERT WITH CHECK (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_kv_store_owner_select" ON "public"."workflow_kv_store" FOR SELECT USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



ALTER TABLE "public"."workflow_webhook_triggers" ENABLE ROW LEVEL SECURITY;


CREATE POLICY "workflow_webhook_triggers_owner_delete" ON "public"."workflow_webhook_triggers" FOR DELETE USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_webhook_triggers_owner_insert" ON "public"."workflow_webhook_triggers" FOR INSERT WITH CHECK (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



CREATE POLICY "workflow_webhook_triggers_owner_select" ON "public"."workflow_webhook_triggers" FOR SELECT USING (("workflow_id" IN ( SELECT "workflows"."id"
   FROM "lenses"."workflows"
  WHERE ("workflows"."lenser_id" = ( SELECT "profiles"."id"
           FROM "lensers"."profiles"
          WHERE ("profiles"."user_id" = "auth"."uid"()))))));



GRANT USAGE ON SCHEMA "public" TO "postgres";
GRANT USAGE ON SCHEMA "public" TO "anon";
GRANT USAGE ON SCHEMA "public" TO "authenticated";
GRANT USAGE ON SCHEMA "public" TO "service_role";



GRANT ALL ON FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."calculate_credit_cost"("p_model_id" "uuid", "p_input_tokens" bigint, "p_output_tokens" bigint, "p_units" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "service_role";
GRANT ALL ON FUNCTION "public"."custom_access_token_hook"("event" "jsonb") TO "supabase_auth_admin";



REVOKE ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn__workspace_ownership_predicate"("p_table_name" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_accept_follow_request"("p_source_profile_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_dlq_entries"("p_unresolved_only" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_stuck_battles"("p_threshold_minutes" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_vote_anomalies"("p_status" "text", "p_battle_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_get_worker_health"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_get_worker_health"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_get_worker_health"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_get_worker_health"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_health"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_health"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_health"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_health"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_admin_retry_dlq"("p_dead_letter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_advance_series"("p_series_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_advance_tournament"("p_match_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_adapters_remove"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_daily_join_count"("p_agent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_list_subscriptions"("p_agent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_run_events"("p_ai_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_subscribe_to_battles"("p_agent_id" "uuid", "p_category" "text", "p_execution_mode" "text", "p_workflow_id" "uuid", "p_require_approval" boolean, "p_max_joins_per_day" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_agent_unsubscribe_from_battles"("p_subscription_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_model_detail"("p_provider_key" "text", "p_model_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_models"("p_provider_key" "text", "p_support_level" "text", "p_capability" "text", "p_modality" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_catalog_providers"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_providers"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_catalog_providers"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ai_get_generations_for_lens"("p_lens_id" "uuid", "p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer, "p_media_kind" "text", "p_ai_model_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_product_feedback_list_user_paginated"("p_offset" integer, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_share_events_log"("p_short_id" "text", "p_event_type" "text", "p_ip_hash" "text", "p_user_agent" "text", "p_referer" "text", "p_country" "text", "p_city" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_consume"("p_short_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_create"("p_resource_type" "text", "p_resource_id" "text", "p_slug" "text", "p_channel" "text", "p_meta" "jsonb", "p_display_name" "text", "p_expires_at" timestamp with time zone, "p_max_uses" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_shared_links_get"("p_short_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_analytics_submit_feedback_public"("p_product_tag" "text", "p_page" "text", "p_message" "text", "p_start_date" timestamp with time zone, "p_end_date" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_append_team_run_event"("p_team_run_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_event"("p_run_id" "uuid", "p_type" "text", "p_payload" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_append_workflow_run_media"("p_run_id" "uuid", "p_object_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_node_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_approve_tool_invocation"("p_invocation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_archive_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_archive_lens"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_archive_workflow"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_archive"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_manage"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_can_view"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_delete"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_dependency_summary"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_lifecycle_status"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_pin"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_pin" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_artifact_restore"("p_artifact_type" "text", "p_artifact_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_assert_modality_allowed"("p_agent_id" "uuid", "p_modality" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_assign_lens_to_contender"("p_contender_id" "uuid", "p_battle_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_input_snapshot" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_assign_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid", "p_profile_id" "uuid", "p_allowed" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_approve_device_request"("p_user_code" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_approval"("p_request_id" "uuid", "p_request_secret" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_exchange_device_login"("p_request_id" "uuid", "p_request_secret" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_list_developer_tokens"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_approval"("p_label" "text", "p_request_ttl_minutes" integer, "p_token_ttl_hours" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_request_device_login"("p_request_ttl_minutes" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_revoke_developer_token"("p_token_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_auth_store_device_login_session"("p_user_code" "text", "p_access_token" "text", "p_refresh_token" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_backfill_schedule"("p_schedule_id" "uuid", "p_since" timestamp with time zone, "p_dry_run" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_badge_check_and_award"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_close_voting"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_dispatch_agent"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_model_spec" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_force_transition"("p_battle_id" "uuid", "p_target_status" "text", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_accept"("p_token" "text", "p_via_qr" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_create"("p_battle_id" "uuid", "p_type" "text", "p_target_handle" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_list"("p_battle_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_invite_stats"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_open_voting"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_set_schedule"("p_battle_id" "uuid", "p_open_at" timestamp with time zone, "p_judge_at" timestamp with time zone, "p_publish_at" timestamp with time zone, "p_auto_judge" boolean, "p_auto_publish" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_submit_workflow"("p_battle_id" "uuid", "p_workflow_id" "uuid", "p_run_id" "uuid", "p_agent_id" "uuid", "p_content" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battle_update_workflow_submission"("p_submission_id" "uuid", "p_content" "text", "p_execution_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_archive"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_promote"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_auto_schedule_contenders"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_change_vote"("p_battle_id" "uuid", "p_new_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_check_readiness"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_clone"("p_battle_id" "uuid", "p_title" "text", "p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_close"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create"("p_title" "text", "p_slug" "text", "p_task_prompt" "text", "p_rubric_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create_from_template"("p_template_id" "uuid", "p_title" "text", "p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create_rematch"("p_parent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_create_template"("p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_delete"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_delete_template"("p_template_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_finalize"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_my_vote"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_public"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_subscriptions"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_get_template"("p_template_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_invite"("p_battle_id" "uuid", "p_email" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_join"("p_battle_id" "uuid", "p_agent_id" "uuid", "p_runner_mode" "text", "p_device_id" "uuid", "p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_leaderboard"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_link_forum_thread"("p_battle_id" "uuid", "p_forum_thread_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_list_public"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_next_recommendation"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_notify_webhooks"("p_battle_id" "uuid", "p_event_type" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_open"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_publish"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_render_prompt"("p_template_id" "uuid", "p_variables" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_restore"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_retract"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_revoke_webhook"("p_subscription_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_series_create"("p_name" "text", "p_seed_battle_id" "uuid", "p_cron_expr" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_start_voting"("p_battle_id" "uuid", "p_voting_closes_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_submit"("p_battle_id" "uuid", "p_content_text" "text", "p_content_url" "text", "p_content_media" "jsonb", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_source_type" "text", "p_adapter_id" "uuid", "p_model_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_submit_media"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_subscribe_webhook"("p_battle_id" "uuid", "p_webhook_url" "text", "p_event_types" "text"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_update_template"("p_template_id" "uuid", "p_title" "text", "p_description" "text", "p_task_prompt" "text", "p_category" "text", "p_max_contenders" integer, "p_is_public" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_battles_vote"("p_battle_id" "uuid", "p_vote" "battles"."vote_value_enum", "p_rationale" "text", "p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_block_profile"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_browse_battles"("p_category" "text", "p_status" "text", "p_q" "text", "p_after_created" timestamp with time zone, "p_after_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_hint"("p_agent_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_register"("p_agent_id" "uuid", "p_provider" "text", "p_key_encrypted" "text", "p_key_hint" "text", "p_label" "text", "p_expires_at" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_resolve_v2"("p_agent_id" "uuid", "p_provider" "text", "p_model_id" "text", "p_reservation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_revoke"("p_agent_id" "uuid", "p_provider" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_key_rotate"("p_agent_id" "uuid", "p_provider" "text", "p_new_encrypted" "text", "p_new_hint" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_log_usage"("p_key_id" "uuid", "p_battle_id" "uuid", "p_model_id" "text", "p_token_count" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_rotation_due"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_rotation_due"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_rotation_due"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_rotation_due"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_send_rotation_notifications"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_byok_validate_for_battle"("p_battle_id" "uuid", "p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_account_deletion_on_login"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_account_deletion_on_login"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_account_deletion_on_login"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_agent_run"("p_team_run_id" "uuid", "p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_all_active_runs"("p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_run"("p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cancel_workflow_run_over_budget"("p_run_id" "uuid", "p_pending_credits" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_and_upsert_aggregate"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text", "p_entity_id" "uuid", "p_window" interval) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_handle"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_handle"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_handle"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_media_quality"("p_submission_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_vote_velocity"("p_voter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_voter_eligibility"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_check_webhook_signing_secret"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_claim_stale_workflow_run"("p_worker_id" "text", "p_stale_after_ms" integer, "p_max_claims" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_clone_lens"("p_source_lens_id" "uuid", "p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_clone_workflow"("p_source_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_complete_onboarding"("p_handle" "text", "p_display_name" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_complete_scratchpad_run"("p_run_id" "uuid", "p_output" "text", "p_status" "text", "p_cost_credits" integer, "p_error" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_complete_tool_invocation"("p_invocation_id" "uuid", "p_status" "text", "p_output" "jsonb", "p_error" "text", "p_cost" numeric) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_compute_elo_after_battle"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_assert_scope"("p_required" "text", "p_granted" "text"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_create"("p_name" "text", "p_slug" "text", "p_description" "text", "p_scopes" "text"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_get"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_get"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_connector_get"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_get"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_remove"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_remove"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_connector_remove"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_remove"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_rotate"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_rotate"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_connector_rotate"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_rotate"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connector_test"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connector_test"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_connector_test"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connector_test"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_connectors_list"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_connectors_list"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_connectors_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_connectors_list"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_create_thread"("p_title" "text", "p_content" "text", "p_visibility" "content"."visibility_enum", "p_tag_ids" "uuid"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_follow_tag"("p_tag_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_followed_tags"("p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_lenses"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_following_threads"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_lenses_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_lenses"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_personal_threads"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_popular_lenses"("p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_threads_by_tag"("p_tag_slug" "text", "p_sort" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_lenses"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_get_trending_threads"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_get_user_for_target"("p_target_type" "text", "p_target_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_reactions_toggle"("p_target_type" "text", "p_target_id" "uuid", "p_reaction" "content"."reaction_enum") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_report"("p_target_type" "text", "p_target_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_tags_create"("p_name" "text", "p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_tags_get_by_slug"("p_slug" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_content_unfollow_tag"("p_tag_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_core_languages_list"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_core_languages_list"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_core_languages_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_core_languages_list"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_commit"("p_reservation_id" "uuid", "p_actual_credits" numeric, "p_actual_usd" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_expire_reservations"("p_max_batch" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_meter_tick"("p_reservation_id" "uuid", "p_running_credits" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_quote"("p_model_id" "uuid", "p_est_input_tokens" integer, "p_est_max_output_tokens" integer, "p_units" numeric) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_release"("p_reservation_id" "uuid", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_cost_reserve"("p_ai_lenser_id" "uuid", "p_pricing_snapshot_id" "uuid", "p_reserved_credits" numeric, "p_reserved_usd" numeric, "p_provider_key" "text", "p_idempotency_key" "text", "p_context" "jsonb", "p_actor_id" "uuid", "p_org_id" "uuid", "p_ttl" interval) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_agent_team"("p_ai_lenser_id" "uuid", "p_name" "text", "p_description" "text", "p_initial_members" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_ai_lenser"("p_owner_lenser_id" "uuid", "p_handle" "text", "p_display_name" "text", "p_ai_model_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_automation_notification"("p_lenser_id" "uuid", "p_title" "text", "p_body" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_battle"("p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_battle_config_snapshot"("p_battle_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_battle_series"("p_template_id" "uuid", "p_title" "text", "p_round_count" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_rubric"("p_evaluation_id" "uuid", "p_criteria" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_evaluation_with_cases"("p_owner_lenser_id" "uuid", "p_ai_lenser_id" "uuid", "p_target_type" "text", "p_target_id" "uuid", "p_name" "text", "p_description" "text", "p_scoring_rules" "jsonb", "p_dataset_uri" "text", "p_cases" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_lens"("p_visibility" "text", "p_template_body" "text", "p_title" "text", "p_description" "text", "p_language_code" "text", "p_params" "jsonb", "p_tag_ids" "uuid"[], "p_parent_lens_id" "uuid", "p_forked_from_execution_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_media_object"("p_workspace_id" "uuid", "p_media_type" "text", "p_mime_type" "text", "p_name" "text", "p_content_text" "text", "p_external_url" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_run_report"("p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_schedule_calendar"("p_name" "text", "p_kind" "text", "p_dates" "date"[], "p_timezone" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_scratchpad_run"("p_ai_lenser_id" "uuid", "p_prompt" "text", "p_model_id" "uuid", "p_metadata" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_tag"("p_name" "text", "p_slug" "text", "p_language_code" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_team_run"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_workflow_run_id" "uuid", "p_workflow_assignment_id" "uuid", "p_team_id" "uuid", "p_approval_status" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_thread_reply"("p_thread_id" "uuid", "p_content" "text", "p_parent_reply_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_tournament"("p_title" "text", "p_format" "text", "p_max_contenders" integer, "p_battle_type" "text", "p_ai_judge_enabled" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_workflow"("p_title" "text", "p_description" "text", "p_visibility" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_workflow_version"("p_workflow_id" "uuid", "p_changelog" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_create_workspace_record"("p_table_name" "text", "p_data" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_deactivate_account"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_deactivate_account"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_deactivate_account"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decide_approval"("p_team_run_id" "uuid", "p_decision" "text", "p_reason" "text", "p_modifications" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decide_moderation_override"("p_battle_id" "uuid", "p_entry_id" "uuid", "p_decision" "text", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decide_tool_invocation"("p_log_id" "uuid", "p_decision" "text", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_decrypt_integration_credential"("p_credential_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_automation_rule"("p_rule_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_lens"("p_lens_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_media_object"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_thread"("p_thread_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_thread_reply"("p_reply_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_edge"("p_edge_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_node"("p_node_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_phase"("p_phase_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_schedule"("p_schedule_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workflow_task"("p_task_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_delete_workspace_item"("p_table_name" "text", "p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_deny_mutation"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_deny_mutation"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_deny_mutation"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_detect_suspicious_voting"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_approve"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_heartbeat"("p_device_id" "uuid", "p_daemon_version" "text", "p_envelope_sig" "text", "p_gateway_status" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_list"("p_trust_level" "text", "p_limit" integer, "p_cursor" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_post_challenge"("p_device_id" "uuid", "p_signature" "text", "p_signed_iat" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_register"("p_name" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_capabilities" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_register_with_key"("p_name" "text", "p_public_key" "text", "p_device_type" "text", "p_os" "text", "p_arch" "text", "p_cli_version" "text", "p_daemon_version" "text", "p_capabilities" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_device_revoke"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_dispatch_scheduled_workflows_with_approval"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_emergency_stop"("p_reason" "text", "p_force_mode" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_evaluate_pre_run_policy"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_context" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_execution_persist_response"("p_lenser_id" "uuid", "p_provider" "text", "p_model" "text", "p_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_expire_byok_keys"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_expire_byok_keys"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_expire_byok_keys"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_expire_byok_keys"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_expire_media_objects"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_expire_media_objects"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_expire_media_objects"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_expire_media_objects"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_expire_stale_approvals"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_expire_stale_approvals"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_expire_stale_approvals"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_export_workspace"("p_ai_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_flag_vote_anomaly"("p_battle_id" "uuid", "p_voter_id" "uuid", "p_type" "text", "p_score" double precision) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_ack_commands"("p_command_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_approve_device"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands"("p_device_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_claim_commands_v2"("p_device_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_heartbeat"("p_device_id" "uuid", "p_public_key" "text", "p_hostname" "text", "p_daemon_version" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_gateway_revoke_device"("p_device_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_active_season"("p_app_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_analytics_summary"("p_ai_lenser_id" "uuid", "p_days" integer, "p_model_key" "text", "p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_automation_feed"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_cost_summary"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_profile_by_profile_id"("p_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_quota_snapshot"("p_ai_lenser_id" "uuid", "p_period_date" "date") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_agent_workspace_bootstrap"("p_profile_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_handicap_policy"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_judge_verdicts"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_model"("p_model_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_ai_provider"("p_provider_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_approval_request"("p_request_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_auth_profile_gate"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_auth_profile_gate"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_auth_profile_gate"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_batch_entity_reactions"("p_entity_type" "text", "p_entity_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle"("p_battle_id" "uuid", "p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_by_slug"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_comments"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_contenders"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_elo_log"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_config"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_execution_jobs"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_full"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_public_execution_jobs"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_results"("p_battle_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_scorecards"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_share_card"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battle_submissions"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_battles_feed"("p_status" "text", "p_battle_type" "text", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_contender_rating"("p_lenser_id" "uuid", "p_category" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_contender_ratings"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_creator_timeseries"("p_lenser_id" "uuid", "p_days" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_dlq_counts"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_dlq_counts"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_counts"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_counts"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_dlq_entries"("p_battle_id" "uuid", "p_unresolved_only" boolean, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_media_attachments"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_counts"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reaction_status"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_reactions_by_lenser"("p_entity_type" "text", "p_entity_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_tag_ids"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_entity_translation"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_baseline"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_evaluation_results"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_execution_artifacts"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_execution_status"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_execution_status"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_execution_status"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_fleet_overview"("p_human_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_follow_status"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_gateway_device_health"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_gateway_device_health"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_gateway_device_health"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_global_lenserboard"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_global_messages"("p_battle_id" "uuid", "p_limit" integer, "p_before_ts" timestamp with time zone, "p_before_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_head_to_head"("p_lenser_a" "uuid", "p_lenser_b" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_human_activity_feed"("p_human_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_judge_calibration"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_latest_draft_battle_by_workflow"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_leaderboard"("p_order_by" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_assignment"("p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_detail_bootstrap"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_execution_history"("p_lens_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_for_execution"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_fork_tree"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_detail"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lens_version_parameters"("p_version_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_activity_timeline"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_badges"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_by_id_full"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_language_preference"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_language_preference"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_language_preference"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_brief"("p_handle" "text", "p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profile_full"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_profiles_brief_batch"("p_lenser_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_reaction_history"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_scores"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_lenser_threads_private"("p_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_media_object"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_media_thumbnail_url"("p_media_id" "uuid", "p_width" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_memory_entries_by_workflow"("p_workflow_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_model_test_runs"("p_battle_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_moderation_decisions_for_owner"("p_status" "text", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_my_api_keys"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_my_api_keys"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_api_keys"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_api_keys"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_cost_reservations"("p_limit" integer, "p_status" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_key_secret"("p_key_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_lensers"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_lensers"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_lensers"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_lenses"("p_offset" integer, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_vote"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_my_workflows"("p_lenser_id" "uuid", "p_offset" integer, "p_limit" integer, "p_visibility" "text", "p_sort" "text", "p_search" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_notification_preferences"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_notification_preferences"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_notification_preferences"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_notifications"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_pending_requests"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_provider_configs"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_recently_active_lensers"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_rubric_criteria"("p_criterion_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_rule_dispatch_summary"("p_days" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_run_details"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_run_provenance"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_run_report"("p_report_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_schedule_calendars"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_calendars"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_calendars"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_schedule_run_history"("p_workflow_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_season_leaderboard"("p_app_id" "uuid", "p_season_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_series"("p_series_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_series"("p_series_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_series"("p_series_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_team_members"("p_team_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_thread_by_id_private"("p_thread_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_page"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_thread_replies_private"("p_thread_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tool_invocation_rollup"("p_ai_lenser_id" "uuid", "p_days" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_bracket"("p_tournament_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_by_slug"("p_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_tournament_contenders"("p_tournament_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_trending_battles"("p_limit" integer, "p_cursor" numeric) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_unread_notification_count"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_unread_notification_count"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_unread_notification_count"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_user_entity_reaction"("p_entity_type" "text", "p_entity_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_version_contracts"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_vote_aggregates"("p_battle_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_bootstrap"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_detail"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_edges"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_node_results"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_nodes"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_media_manifest"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_run_state"("p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedule_history"("p_schedule_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_schedules"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workflow_versions"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_controls"("p_ai_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_get_workspace_settings"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_grant_standing_approval"("p_ai_lenser_id" "uuid", "p_workflow_id" "uuid", "p_gate_kind" "text", "p_hours" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_health"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_health"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_health"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_heartbeat_workflow_run"("p_run_id" "uuid", "p_worker_id" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_logs"("p_human_lenser_id" "uuid", "p_run_id" "uuid", "p_event_type" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_human_fleet_runs"("p_human_lenser_id" "uuid", "p_status" "text", "p_agent_id" "uuid", "p_since" timestamp with time zone, "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_insert_notification"("p_lenser_id" "uuid", "p_type" "text", "p_title" "text", "p_body" "text", "p_action_url" "text", "p_metadata" "jsonb", "p_actor_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_elo_log"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_override"("p_target_entity_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_integrity_probe_moderation_webhook"("p_target_url" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_invite_battle_contender"("p_battle_id" "uuid", "p_slot" "text", "p_contender_type" "text", "p_contender_ref_id" "uuid", "p_display_name" "text", "p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_invoke_tool"("p_team_run_id" "uuid", "p_tool_id" "uuid", "p_ai_lenser_id" "uuid", "p_input" "jsonb", "p_agent_run_step_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_is_notification_blocked"("p_actor_id" "uuid", "p_recipient_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_is_notification_muted"("p_lenser_id" "uuid", "p_type" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_is_super_admin"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_is_super_admin"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_is_super_admin"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_is_super_admin"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_journey_state_get"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_journey_state_get"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_journey_state_get"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_journey_state_get"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_journey_state_mark"("p_step" "text", "p_done" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_activate"("p_scope" "text", "p_target_id" "uuid", "p_reason" "text", "p_expires_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_active"("p_scope" "text", "p_target_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_lift"("p_switch_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_kill_switch_list"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_kill_switch_list"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lens_star"("p_slug" "text", "p_unstar" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_create_profile"("p_handle" "text", "p_display_name" "text", "p_bio" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_follow"("p_following_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_active_profile"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_active_profile"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_active_profile"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_current_id"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_current_id"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_current_id"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_follows"("p_lenser_id" "uuid", "p_type" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_fresh_profile"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_fresh_profile"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_fresh_profile"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_is_in_waitinglist"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_leaderboard"("p_period" "text", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_get_preferences"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_get_preferences"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_preferences"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_preferences"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_profile"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_public_profile"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_suggested"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_get_trending"("p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_is_following"("p_target_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_list"("p_type" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_request_deletion"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_request_deletion"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_request_deletion"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_request_deletion"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_sync_social_links"("p_links" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_toggle_waitinglist"("p_kvkk_approved" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_unfollow"("p_following_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_preferences"("p_data" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lensers_update_profile"("p_data" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_create_version"("p_lens_id" "uuid", "p_template_body" "text", "p_changelog" "text", "p_parent_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_list_versions"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_publish_version"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lenses_starred"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lenses_starred"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lenses_starred"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_lifecycle_is_service_role"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_lifecycle_is_service_role"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_lifecycle_is_service_role"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_action_logs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_lens_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_memories"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_include_redacted" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_model_bindings"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_ownerships"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_run_steps"("p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_teams"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agent_tools"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_agents_by_owner"("p_owner_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_approval_requests"("p_ai_lenser_id" "uuid", "p_approval_status" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_automation_rules"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_battle_templates"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_cases"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_rubrics"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluation_runs"("p_evaluation_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_evaluations"("p_owner_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_gateway_devices"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_lens_versions"("p_lens_id" "uuid", "p_include_archived" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_media_objects"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_memory_access_logs"("p_memory_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_memory_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_model_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_my_private_lenses"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_personality_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_policy_evaluations"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_public_battle_templates"("p_category" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_recent_incidents"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_run_incidents"("p_run_report_id" "uuid", "p_severity" "text", "p_resolved" boolean, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_run_reports"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_scratchpad_runs"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_team_edges"("p_team_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_template_workflows"("p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_threads"("p_cursor" "uuid", "p_limit" integer, "p_tag_slug" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tool_assignments"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tool_profiles"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tools"("p_category" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tools"("p_category" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tools"("p_category" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tools_registry"("p_owner_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_tournaments"("p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_assignments"("p_ai_lenser_id" "uuid", "p_limit" integer, "p_cursor" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_phases"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_run_events"("p_run_id" "uuid", "p_after_event_id" bigint, "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_runs"("p_workflow_id" "uuid", "p_limit" integer, "p_offset" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks"("p_phase_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_list_workflow_tasks_by_workflow"("p_workflow_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_log_artifact_lifecycle_event"("p_artifact_type" "text", "p_artifact_id" "uuid", "p_action" "text", "p_actor_id" "uuid", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_log_model_test_run"("p_battle_id" "uuid", "p_template_id" "uuid", "p_model_provider" "text", "p_model_id" "text", "p_prompt_hash" "text", "p_passed" boolean, "p_duration_ms" integer, "p_raw_output" "jsonb", "p_violations" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_log_page_view"("p_target_type" "public"."page_view_target_enum", "p_target_id" "text", "p_path" "text", "p_referrer" "text", "p_user_agent" "text", "p_client_ip" "inet") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_mark_notifications_read"("p_notification_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_mark_tutorial_complete"("p_tutorial_slug" "text", "p_kind" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_bind_attachment"("p_object_id" "uuid", "p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_finalize_upload"("p_object_id" "uuid", "p_bucket" "text", "p_object_key" "text", "p_byte_size" bigint, "p_checksum" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_proxy_log"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_soft_delete"("p_object_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_media_unbind_attachment"("p_entity_type" "text", "p_entity_id" "uuid", "p_binding_key" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_move_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_notify_battle_result"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_oauth_get_connection_for_refresh"("p_lenser_id" "uuid", "p_ref" "text", "p_workspace_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_list_connections"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_list_connections"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_oauth_list_connections"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_oauth_list_connections"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_oauth_resolve_connection"("p_lenser_id" "uuid", "p_ref" "text", "p_required_scopes" "text"[], "p_workspace_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_oauth_revoke_connection"("p_connection_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_oauth_upsert_connection"("p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_provider" "text", "p_capability" "text", "p_label" "text", "p_access_token" "text", "p_refresh_token" "text", "p_granted_scopes" "text"[], "p_expires_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_ops_submit_contact"("p_name" "text", "p_email" "text", "p_subject" "text", "p_message" "text", "p_kvkk_approved" boolean, "p_ip_address" "text", "p_user_agent" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_pause_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_post_battle_comment"("p_battle_id" "uuid", "p_body" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_post_global_message"("p_battle_id" "uuid", "p_body" "text", "p_sender_handle" "text", "p_sender_role" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_profile_completion_score"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_promote_scratchpad_to_memory"("p_run_id" "uuid", "p_memory_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_publish_battle"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_publish_workflow_version"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_purge_due_accounts"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_purge_due_accounts"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_purge_due_accounts"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_queue_freeze"("p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_queue_unfreeze"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_queue_unfreeze"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_queue_unfreeze"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_read_memory_entries"("p_profile_id" "uuid", "p_scope" "text", "p_limit" integer, "p_team_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_run_incident"("p_run_report_id" "uuid", "p_incident_type" "text", "p_severity" "text", "p_title" "text", "p_description" "text", "p_context" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_run_provenance"("p_source_run_id" "uuid", "p_source_node_id" "uuid", "p_source_output_path" "text", "p_target_run_id" "uuid", "p_target_node_id" "uuid", "p_target_input_path" "text", "p_transform" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_scoring_plugin_signal"("p_battle_id" "uuid", "p_plugin_id" "text", "p_signals" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_record_signed_attestation"("p_run_id" "uuid", "p_envelope_kid" "uuid", "p_envelope_iat" timestamp with time zone, "p_envelope_nonce" "text", "p_canonical_jcs_b64url" "text", "p_signature_b64url" "text", "p_workflow_hash" "text", "p_lens_hash" "text", "p_agent_config_hash" "text", "p_runner_version" "text", "p_cli_version" "text", "p_policy_passed" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_redact_memory_entry"("p_memory_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_redacted_agent_snapshot_hash"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_register_tool"("p_key" "text", "p_name" "text", "p_description" "text", "p_category" "text", "p_schema_input" "jsonb", "p_schema_output" "jsonb", "p_auth_method" "text", "p_requires_approval" boolean, "p_is_dangerous" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_register_tournament_contender"("p_tournament_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reject_follow_request"("p_source_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reject_tool_invocation"("p_invocation_id" "uuid", "p_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_remap_thread_tags"("p_thread_id" "uuid", "p_tag_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_remove_battle_contender"("p_contender_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_remove_follow"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_phases"("p_workflow_id" "uuid", "p_ordered_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_reorder_workflow_tasks"("p_phase_id" "uuid", "p_ordered_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_request_follow"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_request_workspace_deletion"("p_ai_lenser_id" "uuid", "p_reason" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_requeue_battle_job_with_backoff"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_execution_model"("p_provider_override" "text", "p_model_override" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_handle_to_email"("p_handle" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_mentions"("p_ids" "uuid"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_run_incident"("p_incident_id" "uuid", "p_resolution" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resolve_vote_anomaly"("p_anomaly_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_lens"("p_lens_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_restore_workflow_version"("p_version_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_resume_agent"("p_ai_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_retry_agent_run"("p_ai_lenser_id" "uuid", "p_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_retry_dead_letter_battle_job"("p_dead_letter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_agent_ownership"("p_ownership_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_api_key"("p_key_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_standing_approval"("p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_revoke_tool"("p_ai_lenser_id" "uuid", "p_tool_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_rls_unprotected_tables"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_run_evaluation"("p_evaluation_id" "uuid", "p_model_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_run_lens"("p_lens_id" "uuid", "p_version_id" "uuid", "p_model_id" "uuid", "p_inputs" "jsonb", "p_funding_source" "text", "p_byok_key_id" "uuid", "p_idempotency_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_bind_device"("p_runner_id" "uuid", "p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_enable"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_get"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_list"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_list"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_list"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_probe"("p_adapter_id" "uuid", "p_prompt" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_register"("p_name" "text", "p_adapter_type" "text", "p_config" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_runner_remove"("p_adapter_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_save_workflow_node_config"("p_node_id" "uuid", "p_config" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_schedule_account_deletion"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_schedule_account_deletion"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_schedule_account_deletion"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_schedule_battle"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_search_lensers"("p_query" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_search_memory_entries"("p_query" "text", "p_profile_id" "uuid", "p_limit" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_security_definer_no_search_path"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_evaluation_baseline"("p_evaluation_id" "uuid", "p_run_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_runner_paused"("p_ai_lenser_id" "uuid", "p_paused" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_calendar"("p_schedule_id" "uuid", "p_calendar_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_condition"("p_schedule_id" "uuid", "p_condition" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_schedule_inputs_rotation"("p_schedule_id" "uuid", "p_rotation" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_signing_secret"("p_secret" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_set_webhook_strict_signing"("p_strict" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_should_send_notification"("p_recipient_id" "uuid", "p_actor_id" "uuid", "p_type" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_start_tournament"("p_tournament_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_start_workflow_run"("p_workflow_id" "uuid", "p_inputs" "jsonb", "p_global_model_id" "text", "p_idempotency_key" "text", "p_version_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_store_api_key"("p_provider" "text", "p_label" "text", "p_raw_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_submit_contender_entry"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "battles"."vote_value_enum", "p_is_draw" boolean, "p_rationale" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_submit_vote"("p_battle_id" "uuid", "p_voted_contender_id" "uuid", "p_vote_value" "text", "p_is_draw" boolean, "p_rationale" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_summarize_memory_profile"("p_profile_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_switch_active_lenser"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_sync_pull"("p_envelope" "jsonb", "p_object_classes" "text"[], "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_sync_push"("p_envelope" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_sync_status"("p_device_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tag_activity_log"("p_events" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tag_workflow_run"("p_run_id" "uuid", "p_tag" "text", "p_severity" "text", "p_node_id" "uuid", "p_metadata" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tags_get_cloud"("p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_tags_search"("p_query" "text", "p_lang" "text", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_templates_set_recurrence"("p_template_id" "uuid", "p_recurrence_rule" "text", "p_next_run_at" timestamp with time zone, "p_auto_start_delay_hours" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs"("p_stale_threshold_hours" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_timeout_stale_runs_safe"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs_safe"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs_safe"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_timeout_stale_runs_safe"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_automation_rule"("p_rule_id" "uuid", "p_is_active" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_battle_template_public"("p_template_id" "uuid", "p_is_public" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_kill_switch"("p_ai_lenser_id" "uuid", "p_enabled" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_media_visibility"("p_object_id" "uuid", "p_visibility" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_toggle_workflow_schedule"("p_schedule_id" "uuid", "p_is_active" boolean) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_transfer_media_ownership"("p_object_id" "uuid", "p_new_owner_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_trg_battles_status_webhook"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_trg_battles_status_webhook"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_trg_battles_status_webhook"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_trg_votes_velocity_check"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_trg_votes_velocity_check"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_trg_votes_velocity_check"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_unblock_profile"("p_target_profile_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_agent_personality"("p_ai_lenser_id" "uuid", "p_personality_note" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_agent_policy"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_agent_profile"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_battle"("p_battle_id" "uuid", "p_title" "text", "p_task_prompt" "text", "p_battle_type" "text", "p_voter_eligibility" "text", "p_handicap_config" "jsonb", "p_workflow_id" "uuid", "p_lens_id" "uuid", "p_forum_thread_id" "uuid", "p_shared_input_snapshot" "jsonb", "p_lenser_battle_policy" "jsonb", "p_task_source" "text", "p_contender_structure" "text", "p_judging_mode" "text", "p_challenge_type" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_battle_execution_settings"("p_battle_id" "uuid", "p_execution_starts_at" timestamp with time zone, "p_voting_duration_hours" integer, "p_auto_publish" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_team_member_role"("p_team_id" "uuid", "p_member_id" "uuid", "p_role" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_thread_translation"("p_thread_id" "uuid", "p_title" "text", "p_content" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_thread_visibility"("p_thread_id" "uuid", "p_visibility" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_visibility" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_retry_count" integer, "p_duration_ms" integer, "p_ttfb_ms" integer, "p_waiting_reason" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workflow_run_status"("p_run_id" "uuid", "p_status" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_update_workspace_settings"("p_ai_lenser_id" "uuid", "p_patch" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_lens_binding"("p_ai_lenser_id" "uuid", "p_lens_id" "uuid", "p_version_id" "uuid", "p_is_default" boolean, "p_category_tags" "text"[]) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_model_binding"("p_ai_lenser_id" "uuid", "p_model_id" "uuid", "p_is_default" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_ownership"("p_ai_lenser_id" "uuid", "p_owner_lenser_id" "uuid", "p_role" "text", "p_permission_scope" "text"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_agent_run_step"("p_team_run_id" "uuid", "p_workflow_node_id" "uuid", "p_lane" integer, "p_title" "text", "p_status" "text", "p_current_task" "text", "p_recent_output_summary" "text", "p_blocker_summary" "text", "p_started_at" timestamp with time zone, "p_completed_at" timestamp with time zone) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_battle_execution_config"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_provider_key" "text", "p_model_key" "text", "p_model_id" "uuid", "p_funding_source" "text", "p_byok_key_ref_id" "uuid", "p_max_tokens" integer, "p_temperature" numeric) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_notification_preference"("p_type" "text", "p_enabled" boolean) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_provider_config"("p_ai_lenser_id" "uuid", "p_provider_key" "text", "p_base_url" "text", "p_status" "text", "p_ai_key_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_edges"("p_workflow_id" "uuid", "p_edges" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_nodes"("p_workflow_id" "uuid", "p_nodes" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_phase"("p_workflow_id" "uuid", "p_title" "text", "p_description" "text", "p_ordinal" integer, "p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_schedule"("p_workflow_id" "uuid", "p_schedule_id" "uuid", "p_cron_expr" "text", "p_timezone" "text", "p_description" "text", "p_approval_policy" "jsonb", "p_is_active" boolean, "p_global_model_id" "text", "p_assignee_id" "uuid", "p_workflow_assignment_id" "uuid", "p_retry_policy" "jsonb", "p_failure_policy" "jsonb", "p_queue_policy" "jsonb", "p_inputs_template" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workflow_task"("p_phase_id" "uuid", "p_workflow_id" "uuid", "p_title" "text", "p_prompt_text" "text", "p_output_type" "text", "p_model_hint" "text", "p_ordinal" integer, "p_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_upsert_workspace_item"("p_table_name" "text", "p_id" "uuid", "p_patch" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_battle_job_to_dlq"("p_job_id" "uuid", "p_error_code" "text", "p_error_msg" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_battle_job"("p_worker_id" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_queued_run"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_scheduled_workflow_run"("p_worker_id" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_claim_team_run"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_claim_team_run"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_team_run"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_claim_team_run"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_battle_job"("p_job_id" "uuid", "p_status" "text", "p_output_text" "text", "p_error" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_complete_execution_run"("p_run_id" "uuid", "p_status" "text", "p_token_input" integer, "p_token_output" integer, "p_credit_cost" bigint, "p_billing_status" "text", "p_response_text" "text", "p_response_meta" "jsonb", "p_error_code" "text", "p_error_message" "text", "p_latency_ms" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_decrypt_api_key"("p_key_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_fail_execution_run"("p_run_id" "uuid", "p_error_code" "text", "p_error_message" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_ai_key_secret"("p_ai_key_id" "uuid", "p_user_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_judge"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_battle_for_og"("p_battle_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_delegation_context"("p_team_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_lens_template_body"("p_lens_id" "uuid", "p_version_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_team_run"("p_team_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_vote_risk_data"("p_battle_id" "uuid", "p_lenser_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_voter_stats"("p_voter_lenser_id" "uuid", "p_since_ts" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_context"("p_run_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_get_workflow_graph"("p_workflow_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_insert_workflow_media_object"("p_workspace_id" "uuid", "p_owner_lenser_id" "uuid", "p_run_id" "uuid", "p_node_id" "uuid", "p_external_url" "text", "p_mime_type" "text", "p_media_type" "text", "p_name" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_persist_execution_artifacts"("p_run_id" "uuid", "p_lenser_id" "uuid", "p_workspace_id" "uuid", "p_ai_model_id" "uuid", "p_kind" "text", "p_content_text" "text", "p_content_json" "jsonb", "p_media_ids" "uuid"[]) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_render_template"("p_version_id" "uuid", "p_inputs" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_requeue_battle_job"("p_job_id" "uuid", "p_backoff_ms" integer, "p_error" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_run_auto_promote_cycle"() TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_set_battle_og_image"("p_battle_id" "uuid", "p_og_image_url" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_start_media_execution"("p_user_id" "uuid", "p_origin_type" "text", "p_funding_source" "text", "p_lens_id" "uuid", "p_byok_key_ref_id" "uuid", "p_input_snapshot" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_update_team_run_status"("p_team_run_id" "uuid", "p_status" "text", "p_completed_at" timestamp with time zone) TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_update_vote_risk_score"("p_vote_id" "uuid", "p_risk_score" numeric, "p_risk_factors" "text"[], "p_review_status" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_battle_submission"("p_battle_id" "uuid", "p_contender_id" "uuid", "p_content_text" "text", "p_execution_run_id" "uuid", "p_artifact_id" "uuid", "p_is_final" boolean, "p_media_url" "text", "p_mime_type" "text", "p_output_modality" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_heartbeat"("p_worker_id" "text", "p_worker_type" "text", "p_metadata" "jsonb") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_worker_upsert_node_result"("p_run_id" "uuid", "p_node_id" "uuid", "p_status" "text", "p_output_data" "jsonb", "p_error_message" "text", "p_resolved_input_snapshot" "jsonb", "p_provider_route" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_chain_run"("p_parent_run_id" "uuid", "p_child_workflow_id" "uuid", "p_input" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_dispatch_on_event"("p_event_type" "text", "p_event_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_evaluate_condition"("p_condition" "jsonb", "p_event" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_get_popular"("p_offset" integer, "p_limit" integer, "p_search" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_workflows_webhook_trigger"("p_workflow_id" "uuid", "p_secret" "text", "p_payload" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_write_memory_entry"("p_profile_id" "uuid", "p_scope" "text", "p_source" "text", "p_content" "text", "p_confidence" numeric, "p_expires_at" timestamp with time zone, "p_team_run_id" "uuid", "p_metadata" "jsonb") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_freeze_content"("p_source_ref_type" "text", "p_source_ref_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_apps"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_apps"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_apps"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_contributions"("p_lenser_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_history"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_level_ups"("p_lenser_id" "uuid", "p_limit" integer) TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_seasons"("p_app_id" "uuid") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_self"() TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_self"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_self"() TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_streak"("p_lenser_id" "uuid", "p_streak_type" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") TO "anon";
GRANT ALL ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") TO "authenticated";
GRANT ALL ON FUNCTION "public"."fn_xp_get_summary"("p_lenser_id" "uuid", "p_app_id" "uuid") TO "service_role";



REVOKE ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_active_models_by_provider"("p_provider_key" "text") TO "service_role";



REVOKE ALL ON FUNCTION "public"."get_active_providers"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."get_active_providers"() TO "anon";
GRANT ALL ON FUNCTION "public"."get_active_providers"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_active_providers"() TO "service_role";



GRANT ALL ON FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") TO "anon";
GRANT ALL ON FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_model_info"("p_provider_key" "text", "p_model_key" "text") TO "service_role";



GRANT ALL ON FUNCTION "public"."guard_notification_fields"() TO "anon";
GRANT ALL ON FUNCTION "public"."guard_notification_fields"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."guard_notification_fields"() TO "service_role";



GRANT ALL ON FUNCTION "public"."is_admin"() TO "anon";
GRANT ALL ON FUNCTION "public"."is_admin"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."is_admin"() TO "service_role";



GRANT ALL ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "anon";
GRANT ALL ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "authenticated";
GRANT ALL ON FUNCTION "public"."list_tags_stats"("p_lang" "text", "p_limit" integer, "p_offset" integer) TO "service_role";



REVOKE ALL ON FUNCTION "public"."set_updated_at"() FROM PUBLIC;
GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "anon";
GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."set_updated_at"() TO "service_role";



GRANT ALL ON FUNCTION "public"."trg_block_unsafe_artifact_delete"() TO "anon";
GRANT ALL ON FUNCTION "public"."trg_block_unsafe_artifact_delete"() TO "authenticated";
GRANT ALL ON FUNCTION "public"."trg_block_unsafe_artifact_delete"() TO "service_role";



GRANT ALL ON TABLE "public"."contact_messages" TO "anon";
GRANT ALL ON TABLE "public"."contact_messages" TO "authenticated";
GRANT ALL ON TABLE "public"."contact_messages" TO "service_role";



GRANT ALL ON TABLE "public"."notification_aggregates" TO "anon";
GRANT ALL ON TABLE "public"."notification_aggregates" TO "authenticated";
GRANT ALL ON TABLE "public"."notification_aggregates" TO "service_role";



GRANT ALL ON TABLE "public"."notification_preferences" TO "anon";
GRANT ALL ON TABLE "public"."notification_preferences" TO "authenticated";
GRANT ALL ON TABLE "public"."notification_preferences" TO "service_role";



GRANT INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,MAINTAIN,UPDATE ON TABLE "public"."notifications" TO "anon";
GRANT ALL ON TABLE "public"."notifications" TO "authenticated";
GRANT ALL ON TABLE "public"."notifications" TO "service_role";



GRANT MAINTAIN ON TABLE "public"."user_oauth_connections" TO "anon";
GRANT MAINTAIN ON TABLE "public"."user_oauth_connections" TO "authenticated";
GRANT ALL ON TABLE "public"."user_oauth_connections" TO "service_role";



GRANT ALL ON TABLE "public"."v_workflow_run_cost_breakdown" TO "anon";
GRANT ALL ON TABLE "public"."v_workflow_run_cost_breakdown" TO "authenticated";
GRANT ALL ON TABLE "public"."v_workflow_run_cost_breakdown" TO "service_role";



GRANT ALL ON TABLE "public"."v_workflow_run_health" TO "anon";
GRANT ALL ON TABLE "public"."v_workflow_run_health" TO "authenticated";
GRANT ALL ON TABLE "public"."v_workflow_run_health" TO "service_role";



GRANT ALL ON TABLE "public"."v_workflow_run_timeline" TO "anon";
GRANT ALL ON TABLE "public"."v_workflow_run_timeline" TO "authenticated";
GRANT ALL ON TABLE "public"."v_workflow_run_timeline" TO "service_role";



GRANT ALL ON TABLE "public"."vw_ai_models_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_ai_models_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_ai_models_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_auth_lenser" TO "anon";
GRANT ALL ON TABLE "public"."vw_auth_lenser" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_auth_lenser" TO "service_role";



GRANT ALL ON TABLE "public"."vw_battle_funnel" TO "anon";
GRANT ALL ON TABLE "public"."vw_battle_funnel" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battle_funnel" TO "service_role";



GRANT ALL ON TABLE "public"."vw_battle_health" TO "anon";
GRANT ALL ON TABLE "public"."vw_battle_health" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battle_health" TO "service_role";



GRANT ALL ON TABLE "public"."vw_battle_participation" TO "anon";
GRANT ALL ON TABLE "public"."vw_battle_participation" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battle_participation" TO "service_role";



GRANT ALL ON TABLE "public"."vw_battles_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_battles_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_battles_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_content_tags_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_content_tags_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_content_tags_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_content_thread_replies_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_content_thread_replies_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_content_thread_replies_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_content_threads_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_content_threads_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_content_threads_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_feedback_admin" TO "anon";
GRANT ALL ON TABLE "public"."vw_feedback_admin" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_feedback_admin" TO "service_role";



GRANT ALL ON TABLE "public"."vw_feedback_user" TO "anon";
GRANT ALL ON TABLE "public"."vw_feedback_user" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_feedback_user" TO "service_role";



GRANT ALL ON TABLE "public"."vw_global_messages" TO "anon";
GRANT ALL ON TABLE "public"."vw_global_messages" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_global_messages" TO "service_role";



GRANT ALL ON TABLE "public"."vw_lensers_public_recent" TO "anon";
GRANT ALL ON TABLE "public"."vw_lensers_public_recent" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lensers_public_recent" TO "service_role";



GRANT ALL ON TABLE "public"."vw_lensers_social_links_private" TO "anon";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_private" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_private" TO "service_role";



GRANT ALL ON TABLE "public"."vw_lensers_social_links_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lensers_social_links_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_lenses_public" TO "anon";
GRANT ALL ON TABLE "public"."vw_lenses_public" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_lenses_public" TO "service_role";



GRANT ALL ON TABLE "public"."vw_tags_public_extended" TO "anon";
GRANT ALL ON TABLE "public"."vw_tags_public_extended" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_tags_public_extended" TO "service_role";



GRANT ALL ON TABLE "public"."vw_tags_public_stats" TO "anon";
GRANT ALL ON TABLE "public"."vw_tags_public_stats" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_tags_public_stats" TO "service_role";



GRANT ALL ON TABLE "public"."vw_workflows" TO "anon";
GRANT ALL ON TABLE "public"."vw_workflows" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_workflows" TO "service_role";



GRANT ALL ON TABLE "public"."vw_xp_leaderboard_global" TO "anon";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_global" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_global" TO "service_role";



GRANT ALL ON TABLE "public"."vw_xp_leaderboard_season" TO "anon";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_season" TO "authenticated";
GRANT ALL ON TABLE "public"."vw_xp_leaderboard_season" TO "service_role";



GRANT ALL ON TABLE "public"."workflow_integration_credentials" TO "anon";
GRANT ALL ON TABLE "public"."workflow_integration_credentials" TO "authenticated";
GRANT ALL ON TABLE "public"."workflow_integration_credentials" TO "service_role";



GRANT ALL ON TABLE "public"."workflow_kv_store" TO "anon";
GRANT ALL ON TABLE "public"."workflow_kv_store" TO "authenticated";
GRANT ALL ON TABLE "public"."workflow_kv_store" TO "service_role";



GRANT ALL ON TABLE "public"."workflow_webhook_triggers" TO "anon";
GRANT ALL ON TABLE "public"."workflow_webhook_triggers" TO "authenticated";
GRANT ALL ON TABLE "public"."workflow_webhook_triggers" TO "service_role";



ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";






ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";






ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";







