-- Weeks 7–8: durable world projections, PostgreSQL context retrieval, quotas,
-- usage telemetry, atomic summaries, and owner-audited corrections.

alter table public.ai_usage add column if not exists usage_key text;
alter table public.ai_usage add column if not exists provider text not null default 'openrouter';
alter table public.ai_usage add column if not exists request_kind text not null default 'unknown';
update public.ai_usage set usage_key = id::text where usage_key is null;
alter table public.ai_usage alter column usage_key set not null;
create unique index if not exists ai_usage_usage_key_unique on public.ai_usage(usage_key);
create index if not exists ai_usage_user_created_idx on public.ai_usage(user_id, created_at desc);
create index if not exists ai_usage_campaign_created_idx on public.ai_usage(campaign_id, created_at desc);

create table if not exists public.ai_quota_events (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references public.profiles(id) on delete cascade,
  campaign_id uuid references public.campaigns(id) on delete cascade,
  request_kind text not null check (char_length(request_kind) between 1 and 80),
  created_at timestamptz not null default now()
);
create index if not exists ai_quota_events_user_created_idx on public.ai_quota_events(user_id, created_at desc);
create index if not exists ai_quota_events_campaign_created_idx on public.ai_quota_events(campaign_id, created_at desc);
alter table public.ai_quota_events enable row level security;
drop policy if exists quota_events_self_read on public.ai_quota_events;
create policy quota_events_self_read on public.ai_quota_events for select using (user_id = auth.uid());

create table if not exists public.scene_states (
  campaign_id uuid primary key references public.campaigns(id) on delete cascade,
  location_id uuid references public.world_entities(id) on delete set null,
  summary text not null check (char_length(summary) between 1 and 6000),
  active_entity_ids uuid[] not null default '{}',
  tags text[] not null default '{}',
  through_round integer not null default 0 check (through_round >= 0),
  updated_at timestamptz not null default now()
);

create table if not exists public.quest_states (
  campaign_id uuid not null references public.campaigns(id) on delete cascade,
  quest_entity_id uuid not null references public.world_entities(id) on delete cascade,
  status text not null default 'hidden' check (status in ('hidden', 'active', 'completed', 'failed')),
  summary text not null check (char_length(summary) between 1 and 1200),
  tags text[] not null default '{}',
  through_round integer not null default 0 check (through_round >= 0),
  updated_at timestamptz not null default now(),
  primary key (campaign_id, quest_entity_id)
);
create index if not exists quest_states_campaign_status_idx on public.quest_states(campaign_id, status, updated_at desc);

create or replace function public.stage_six_ensure_opening_quest()
returns trigger language plpgsql security definer set search_path = '' as $$
begin
  if nullif(btrim(new.hook), '') is not null and not exists (
    select 1 from public.world_entities where world_id = new.id and kind = 'quest'
  ) then
    insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
    values (new.id, 'quest', 'Opening objective', left(btrim(new.hook), 1200), array['opening', 'active'], '[]'::jsonb);
  end if;
  return new;
end;
$$;

drop trigger if exists stage_six_world_opening_quest on public.worlds;
create trigger stage_six_world_opening_quest
after insert or update of hook on public.worlds
for each row execute function public.stage_six_ensure_opening_quest();

insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
select world.id, 'quest', 'Opening objective', left(btrim(world.hook), 1200), array['opening', 'active'], '[]'::jsonb
from public.worlds world
where nullif(btrim(world.hook), '') is not null
  and not exists (select 1 from public.world_entities entity where entity.world_id = world.id and entity.kind = 'quest');

alter table public.scene_states enable row level security;
alter table public.quest_states enable row level security;
drop policy if exists scene_states_member_read on public.scene_states;
drop policy if exists quest_states_member_read on public.quest_states;
create policy scene_states_member_read on public.scene_states for select using (
  public.is_campaign_member(campaign_id) or public.is_campaign_owner(campaign_id)
);
create policy quest_states_member_read on public.quest_states for select using (
  public.is_campaign_member(campaign_id) or public.is_campaign_owner(campaign_id)
);

insert into public.scene_states(campaign_id, summary)
select id, current_scene from public.campaigns
on conflict (campaign_id) do nothing;

insert into public.quest_states(campaign_id, quest_entity_id, status, summary, tags)
select campaign.id, entity.id, case when 'opening' = any(entity.tags) then 'active' else 'hidden' end, entity.summary, entity.tags
from public.campaigns campaign
join public.world_entities entity on entity.world_id = campaign.world_id and entity.kind = 'quest'
on conflict (campaign_id, quest_entity_id) do nothing;

create or replace function public.stage_six_initialize_campaign_state()
returns trigger language plpgsql security definer set search_path = '' as $$
begin
  insert into public.scene_states(campaign_id, summary)
  values (new.id, new.current_scene)
  on conflict (campaign_id) do nothing;

  insert into public.quest_states(campaign_id, quest_entity_id, status, summary, tags)
  select new.id, entity.id, case when 'opening' = any(entity.tags) then 'active' else 'hidden' end, entity.summary, entity.tags
  from public.world_entities entity
  where entity.world_id = new.world_id and entity.kind = 'quest'
  on conflict (campaign_id, quest_entity_id) do nothing;
  return new;
end;
$$;

drop trigger if exists stage_six_campaign_state on public.campaigns;
create trigger stage_six_campaign_state
after insert on public.campaigns
for each row execute function public.stage_six_initialize_campaign_state();

create or replace function public.stage_six_consume_ai_quota(
  p_user_id uuid,
  p_campaign_id uuid,
  p_request_kind text
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
  v_user_minute integer;
  v_user_day integer;
  v_campaign_day integer := 0;
  v_tokens_day bigint;
begin
  if p_user_id is null or not exists (select 1 from public.profiles where id = p_user_id) then
    raise exception 'AI quota requires a valid user';
  end if;
  if p_campaign_id is not null and not exists (
    select 1 from public.campaigns where id = p_campaign_id
      and (owner_id = p_user_id or exists (
        select 1 from public.campaign_members where campaign_id = p_campaign_id and user_id = p_user_id and active
      ))
  ) then
    raise exception 'AI quota campaign access denied';
  end if;
  if p_request_kind is null or char_length(btrim(p_request_kind)) not between 1 and 80 then
    raise exception 'AI request kind is required';
  end if;

  perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended('ai-quota:' || p_user_id::text, 0));
  select count(*) into v_user_minute from public.ai_quota_events
  where user_id = p_user_id and created_at >= now() - interval '1 minute';
  select count(*) into v_user_day from public.ai_quota_events
  where user_id = p_user_id and created_at >= date_trunc('day', now());
  select coalesce(sum(input_tokens + output_tokens), 0) into v_tokens_day from public.ai_usage
  where user_id = p_user_id and created_at >= date_trunc('day', now());
  if p_campaign_id is not null then
    select count(*) into v_campaign_day from public.ai_quota_events
    where campaign_id = p_campaign_id and created_at >= date_trunc('day', now());
  end if;

  if v_user_minute >= 10 then raise exception 'AI rate limit reached; try again in a minute'; end if;
  if v_user_day >= 60 then raise exception 'daily user AI quota reached'; end if;
  if p_campaign_id is not null and v_campaign_day >= 40 then raise exception 'daily campaign AI quota reached'; end if;
  if v_tokens_day >= 250000 then raise exception 'daily AI token quota reached'; end if;

  insert into public.ai_quota_events(user_id, campaign_id, request_kind)
  values (p_user_id, p_campaign_id, btrim(p_request_kind));
  return jsonb_build_object(
    'userRemaining', 59 - v_user_day,
    'campaignRemaining', case when p_campaign_id is null then null else 39 - v_campaign_day end,
    'tokenRemaining', greatest(0, 250000 - v_tokens_day)
  );
end;
$$;

create or replace function public.enqueue_round_resolution(
  p_round_id uuid,
  p_forced_by uuid default null
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
  v_round public.rounds%rowtype;
  v_job_id uuid;
  v_owner_id uuid;
begin
  select * into v_round from public.rounds where id = p_round_id for update;
  if not found then raise exception 'round not found'; end if;
  select owner_id into v_owner_id from public.campaigns where id = v_round.campaign_id;

  select id into v_job_id from public.ai_jobs
  where job_type = 'resolve-round' and entity_id = p_round_id;
  if v_round.status in ('queued', 'resolving', 'resolved') then
    if v_job_id is null and v_round.status <> 'resolved' then raise exception 'round status and job outbox are inconsistent'; end if;
    return v_job_id;
  end if;
  if v_round.status <> 'open' then raise exception 'round cannot be queued from status %', v_round.status; end if;

  if p_forced_by is not null then
    if p_forced_by <> v_owner_id then raise exception 'only the campaign owner can force a round'; end if;
  elsif exists (
    select 1 from public.campaign_members member
    join public.characters character on character.campaign_id = member.campaign_id
      and character.user_id = member.user_id and character.controller = 'human'::public.character_controller
    where member.campaign_id = v_round.campaign_id and member.active
      and not exists (
        select 1 from public.player_intents intent
        where intent.round_id = p_round_id and intent.member_id = member.id and intent.ready
      )
  ) then
    raise exception 'not all active players are ready';
  end if;

  perform public.stage_six_consume_ai_quota(v_owner_id, v_round.campaign_id, 'resolve-round');
  v_job_id := gen_random_uuid();
  insert into public.ai_jobs(id, job_type, entity_id, idempotency_key, status)
  values (v_job_id, 'resolve-round', p_round_id, v_job_id::text, 'queued');
  update public.rounds set status = 'queued', queued_at = now(), forced_by = p_forced_by, error = null
  where id = p_round_id;
  return v_job_id;
end;
$$;

create or replace function public.stage_two_enqueue_world_generation(
  p_session_id uuid,
  p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
  v_session public.coauthor_sessions%rowtype;
  v_job public.ai_jobs%rowtype;
  v_existing boolean := false;
begin
  select * into v_session from public.coauthor_sessions
  where id = p_session_id and owner_id = p_owner_id for update;
  if not found then raise exception 'coauthor session not found'; end if;
  if v_session.status = 'confirmed' then raise exception 'coauthor session is already confirmed'; end if;
  if jsonb_array_length(v_session.messages) < 1 then raise exception 'at least one message is required'; end if;

  select * into v_job from public.ai_jobs
  where job_type = 'generate-world' and entity_id = p_session_id for update;
  v_existing := found;
  if v_existing and v_job.status in ('queued', 'running') then return v_job.id; end if;

  perform public.stage_six_consume_ai_quota(p_owner_id, null, 'generate-world');
  if v_existing then
    update public.ai_jobs set status = 'queued', attempts = 0, claimed_by = null,
      lease_expires_at = null, available_at = now(), error = null, updated_at = now()
    where id = v_job.id;
  else
    v_job.id := gen_random_uuid();
    insert into public.ai_jobs(id, job_type, entity_id, idempotency_key, status)
    values (v_job.id, 'generate-world', p_session_id, v_job.id::text, 'queued');
  end if;
  update public.coauthor_sessions set status = 'generating', generated_world = null, updated_at = now()
  where id = p_session_id;
  return v_job.id;
end;
$$;

create or replace function public.stage_six_retrieve_context(
  p_campaign_id uuid,
  p_search_text text,
  p_limit integer default 8
) returns jsonb language plpgsql stable security definer set search_path = '' as $$
declare
  v_world_id uuid;
  v_query tsquery;
  v_limit integer := greatest(1, least(coalesce(p_limit, 8), 12));
  v_entities jsonb;
  v_entity_ids uuid[];
begin
  select world_id into v_world_id from public.campaigns where id = p_campaign_id;
  if not found then raise exception 'campaign not found'; end if;
  v_query := plainto_tsquery('english', left(coalesce(p_search_text, ''), 4000));

  select coalesce(jsonb_agg(item.payload order by item.relevance desc, item.name), '[]'::jsonb),
         coalesce(array_agg(item.id order by item.relevance desc, item.name), '{}')
  into v_entities, v_entity_ids
  from (
    select entity.id, entity.name,
      jsonb_build_object('id', entity.id, 'kind', entity.kind, 'name', entity.name, 'summary', entity.summary, 'tags', entity.tags) as payload,
      case when numnode(v_query) > 0 then ts_rank_cd(entity.search_document, v_query) else 0 end
        + case when exists (select 1 from unnest(entity.tags) tag where position(lower(tag) in lower(coalesce(p_search_text, ''))) > 0) then 2 else 0 end
        + case when quest.status = 'active' then 3 else 0 end
        + case when scene.location_id = entity.id or entity.id = any(scene.active_entity_ids) then 4 else 0 end as relevance
    from public.world_entities entity
    left join public.quest_states quest on quest.campaign_id = p_campaign_id and quest.quest_entity_id = entity.id
    left join public.scene_states scene on scene.campaign_id = p_campaign_id
    where entity.world_id = v_world_id
    order by relevance desc, entity.created_at
    limit v_limit
  ) item;

  return jsonb_build_object(
    'entities', v_entities,
    'memories', coalesce((
      select jsonb_agg(memory_item.payload order by memory_item.relevance desc, memory_item.created_at desc)
      from (
        select memory.created_at,
          jsonb_build_object('summary', memory.summary, 'importance', memory.importance, 'tags', memory.tags, 'entityIds', memory.entity_ids) as payload,
          memory.importance
            + case when numnode(v_query) > 0 then ts_rank_cd(memory.search_document, v_query) * 10 else 0 end
            + case when memory.entity_ids && v_entity_ids then 5 else 0 end
            + case when exists (select 1 from unnest(memory.tags) tag where position(lower(tag) in lower(coalesce(p_search_text, ''))) > 0) then 4 else 0 end as relevance
        from public.memories memory
        where memory.campaign_id = p_campaign_id
        order by relevance desc, memory.created_at desc
        limit v_limit
      ) memory_item
    ), '[]'::jsonb),
    'relationships', coalesce((
      select jsonb_agg(jsonb_build_object(
        'sourceEntityId', item.source_entity_id,
        'targetEntityId', item.target_entity_id,
        'score', item.score,
        'notes', item.notes
      ) order by abs(item.score) desc)
      from (
        select relationship.* from public.relationships relationship
        where relationship.campaign_id = p_campaign_id
          and (
            relationship.source_entity_id = any(v_entity_ids)
            or relationship.target_entity_id = any(v_entity_ids)
            or exists (
              select 1 from public.characters character
              where character.campaign_id = p_campaign_id
                and character.id in (relationship.source_entity_id, relationship.target_entity_id)
            )
          )
        order by abs(relationship.score) desc
        limit 20
      ) item
    ), '[]'::jsonb),
    'activeGoals', coalesce((
      select jsonb_agg(jsonb_build_object(
        'questEntityId', item.quest_entity_id,
        'name', item.name,
        'status', item.status,
        'summary', item.summary,
        'tags', item.tags
      ) order by item.updated_at desc)
      from (
        select quest.*, entity.name from public.quest_states quest
        join public.world_entities entity on entity.id = quest.quest_entity_id
        where quest.campaign_id = p_campaign_id and quest.status = 'active'
        order by quest.updated_at desc
        limit 8
      ) item
    ), '[]'::jsonb)
  );
end;
$$;

create or replace function public.stage_six_apply_round_projections(
  p_round_id uuid,
  p_events jsonb
) returns void language plpgsql security definer set search_path = '' as $$
declare
  v_round public.rounds%rowtype;
  v_event jsonb;
  v_actor uuid;
  v_target uuid;
  v_before jsonb;
  v_after jsonb;
  v_scene_projected boolean := false;
begin
  select * into v_round from public.rounds where id = p_round_id;
  if not found then raise exception 'round not found'; end if;

  for v_event in select * from jsonb_array_elements(coalesce(p_events, '[]'::jsonb)) loop
    v_actor := nullif(v_event->>'actorId', '')::uuid;
    v_target := nullif(v_event->>'targetId', '')::uuid;

    if v_event->>'type' = 'relationship' then
      if v_actor is null or v_target is null or v_actor = v_target
         or (v_event->>'value')::integer not between -20 and 20 then
        raise exception 'invalid relationship projection';
      end if;
      select to_jsonb(relationship) into v_before from public.relationships relationship
      where campaign_id = v_round.campaign_id and source_entity_id = v_actor and target_entity_id = v_target;
      insert into public.relationships(campaign_id, source_entity_id, target_entity_id, score, notes)
      values (v_round.campaign_id, v_actor, v_target, (v_event->>'value')::integer, v_event->>'description')
      on conflict (campaign_id, source_entity_id, target_entity_id) do update
      set score = greatest(-100, least(100, public.relationships.score + excluded.score)), notes = excluded.notes;
      select to_jsonb(relationship) into v_after from public.relationships relationship
      where campaign_id = v_round.campaign_id and source_entity_id = v_actor and target_entity_id = v_target;
      insert into public.audit_entries(campaign_id, action, entity_type, entity_id, before_state, after_state)
      values (v_round.campaign_id, 'project_relationship', 'relationship', v_target, v_before, v_after);
    elsif v_event->>'type' = 'quest' then
      if v_target is null or v_event->>'item' not in ('hidden', 'active', 'completed', 'failed') then
        raise exception 'invalid quest projection';
      end if;
      select to_jsonb(quest) into v_before from public.quest_states quest
      where campaign_id = v_round.campaign_id and quest_entity_id = v_target;
      update public.quest_states set status = v_event->>'item', summary = v_event->>'description',
        through_round = v_round.number, updated_at = now()
      where campaign_id = v_round.campaign_id and quest_entity_id = v_target;
      if not found then raise exception 'quest is outside the round campaign'; end if;
      select to_jsonb(quest) into v_after from public.quest_states quest
      where campaign_id = v_round.campaign_id and quest_entity_id = v_target;
      insert into public.audit_entries(campaign_id, action, entity_type, entity_id, before_state, after_state)
      values (v_round.campaign_id, 'project_quest', 'quest', v_target, v_before, v_after);
    elsif v_event->>'type' = 'narrative' and v_event->>'item' = 'scene' then
      v_scene_projected := true;
      if v_target is not null and not exists (
        select 1 from public.world_entities entity join public.campaigns campaign on campaign.world_id = entity.world_id
        where campaign.id = v_round.campaign_id and entity.id = v_target and entity.kind = 'location'
      ) then raise exception 'scene location is outside the campaign world'; end if;
      insert into public.scene_states(campaign_id, location_id, summary, active_entity_ids, through_round, updated_at)
      values (
        v_round.campaign_id, v_target, v_event->>'description',
        array_remove(array[v_actor, v_target], null), v_round.number, now()
      )
      on conflict (campaign_id) do update set location_id = excluded.location_id,
        summary = excluded.summary, active_entity_ids = excluded.active_entity_ids,
        through_round = excluded.through_round, updated_at = excluded.updated_at;
      update public.campaigns set current_scene = v_event->>'description', updated_at = now()
      where id = v_round.campaign_id;
    end if;
  end loop;

  if not v_scene_projected and nullif(btrim(v_round.narration), '') is not null then
    insert into public.scene_states(campaign_id, summary, through_round, updated_at)
    values (v_round.campaign_id, v_round.narration, v_round.number, now())
    on conflict (campaign_id) do update set summary = excluded.summary,
      through_round = excluded.through_round, updated_at = excluded.updated_at;
  end if;
end;
$$;

create or replace function public.commit_stage_six_round_resolution(
  p_round_id uuid,
  p_narration text,
  p_next_prompt text,
  p_rolls jsonb,
  p_events jsonb,
  p_character_states jsonb,
  p_memory jsonb,
  p_story_summary text,
  p_idempotency_key text
) returns void language plpgsql security definer set search_path = '' as $$
declare v_round public.rounds%rowtype;
begin
  select * into v_round from public.rounds where id = p_round_id for update;
  if not found then raise exception 'round not found'; end if;
  if v_round.status = 'resolved' then return; end if;
  perform public.commit_srd_round_resolution(
    p_round_id, p_narration, p_next_prompt, p_rolls, p_events,
    p_character_states, p_memory, p_idempotency_key
  );
  perform public.stage_six_apply_round_projections(p_round_id, p_events);
  if p_story_summary is not null then
    if v_round.number % 3 <> 0 or char_length(btrim(p_story_summary)) not between 20 and 6000 then
      raise exception 'invalid scheduled story summary';
    end if;
    insert into public.story_summaries(campaign_id, through_round, summary)
    values (v_round.campaign_id, v_round.number, btrim(p_story_summary))
    on conflict (campaign_id, through_round) do update set summary = excluded.summary;
  end if;
end;
$$;

create or replace function public.commit_claimed_stage_six_round_resolution(
  p_round_id uuid,
  p_narration text,
  p_next_prompt text,
  p_rolls jsonb,
  p_events jsonb,
  p_character_states jsonb,
  p_memory jsonb,
  p_story_summary text,
  p_idempotency_key text,
  p_worker_id text
) returns void language plpgsql security definer set search_path = '' as $$
declare v_round public.rounds%rowtype;
begin
  select * into v_round from public.rounds where id = p_round_id for update;
  if not found then raise exception 'round not found'; end if;
  if v_round.status = 'resolved' then return; end if;
  perform public.commit_claimed_srd_round_resolution(
    p_round_id, p_narration, p_next_prompt, p_rolls, p_events,
    p_character_states, p_memory, p_idempotency_key, p_worker_id
  );
  perform public.stage_six_apply_round_projections(p_round_id, p_events);
  if p_story_summary is not null then
    if v_round.number % 3 <> 0 or char_length(btrim(p_story_summary)) not between 20 and 6000 then
      raise exception 'invalid scheduled story summary';
    end if;
    insert into public.story_summaries(campaign_id, through_round, summary)
    values (v_round.campaign_id, v_round.number, btrim(p_story_summary))
    on conflict (campaign_id, through_round) do update set summary = excluded.summary;
  end if;
end;
$$;

create or replace function public.stage_six_adjust_character_state(
  p_campaign_id uuid,
  p_owner_id uuid,
  p_character_id uuid,
  p_patch jsonb,
  p_reason text
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
  v_character public.characters%rowtype;
  v_before jsonb;
  v_after jsonb;
  v_hp integer;
begin
  if not exists (select 1 from public.campaigns where id = p_campaign_id and owner_id = p_owner_id) then
    raise exception 'only the campaign owner can adjust state';
  end if;
  if p_reason is null or char_length(btrim(p_reason)) not between 3 and 500 then raise exception 'an audit reason is required'; end if;
  if coalesce(jsonb_typeof(p_patch), '') <> 'object' or p_patch - array['hp', 'inventory', 'statuses']::text[] <> '{}'::jsonb then
    raise exception 'only hp, inventory, and statuses may be adjusted';
  end if;
  select * into v_character from public.characters
  where id = p_character_id and campaign_id = p_campaign_id for update;
  if not found then raise exception 'character not found'; end if;
  v_before := jsonb_build_object('hp', v_character.hp, 'inventory', v_character.inventory, 'statuses', v_character.statuses);
  v_hp := coalesce((p_patch->>'hp')::integer, v_character.hp);
  if v_hp < 0 or v_hp > v_character.max_hp then raise exception 'hp must be between zero and max hp'; end if;
  if p_patch ? 'inventory' and jsonb_typeof(p_patch->'inventory') <> 'array' then raise exception 'inventory must be an array'; end if;
  if p_patch ? 'statuses' and jsonb_typeof(p_patch->'statuses') <> 'array' then raise exception 'statuses must be an array'; end if;
  update public.characters set hp = v_hp,
    inventory = coalesce(p_patch->'inventory', inventory),
    statuses = coalesce(p_patch->'statuses', statuses)
  where id = p_character_id
  returning jsonb_build_object('hp', hp, 'inventory', inventory, 'statuses', statuses) into v_after;
  insert into public.audit_entries(campaign_id, actor_id, action, entity_type, entity_id, before_state, after_state)
  values (p_campaign_id, p_owner_id, 'manual_character_adjustment: ' || btrim(p_reason), 'character', p_character_id, v_before, v_after);
  return v_after;
end;
$$;

create or replace function public.dng_schema_version()
returns integer language sql stable security definer set search_path = '' as $$
  select 13;
$$;

revoke all on function public.stage_six_consume_ai_quota(uuid, uuid, text) from public, anon, authenticated;
revoke all on function public.stage_six_retrieve_context(uuid, text, integer) from public, anon, authenticated;
revoke all on function public.stage_six_apply_round_projections(uuid, jsonb) from public, anon, authenticated;
revoke all on function public.commit_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text) from public, anon, authenticated;
revoke all on function public.commit_claimed_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text, text) from public, anon, authenticated;
revoke all on function public.stage_six_adjust_character_state(uuid, uuid, uuid, jsonb, text) from public, anon, authenticated;
revoke all on function public.dng_schema_version() from public, anon, authenticated;
grant execute on function public.stage_six_consume_ai_quota(uuid, uuid, text) to service_role;
grant execute on function public.stage_six_retrieve_context(uuid, text, integer) to service_role;
grant execute on function public.commit_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text) to service_role;
grant execute on function public.commit_claimed_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text, text) to service_role;
grant execute on function public.stage_six_adjust_character_state(uuid, uuid, uuid, jsonb, text) to service_role;
grant execute on function public.dng_schema_version() to service_role;

notify pgrst, 'reload schema';
