-- Immutable Skill package identity for Agentic Rectification and Personal Reports.
-- Additive only: existing records with incomplete provenance remain readable.
-- New Agentic cases bind full identity through the v2 RPC; new Personal Reports
-- bind it through the server-owned persistence service.

do $migration$
begin
  if current_user <> 'schema_owner' then
    raise exception 'immutable_skill_registry_requires_schema_owner'
      using errcode = '42501';
  end if;
end
$migration$;

alter table public.agentic_rectification_cases
  add column if not exists skill_sha256 text
    check (skill_sha256 is null or skill_sha256 ~ '^[0-9a-f]{64}$'),
  add column if not exists skill_source_commit text
    check (skill_source_commit is null or skill_source_commit ~ '^[0-9a-f]{40}$');

-- All Case writes are mediated by owner-held SECURITY DEFINER RPCs. The
-- runtime role must not be able to forge a Case identity through direct INSERT
-- or bypass lifecycle validation through direct UPDATE/DELETE/TRUNCATE.
alter table public.agentic_rectification_cases owner to current_user;
revoke all on table public.agentic_rectification_cases from service_role;

-- ReportDocument v1 predates named/versioned Skill identities. Keep these
-- columns nullable so historical ready rows remain readable; all newly created
-- reports bind both fields through the server-owned persistence service.
alter table public.personal_reports
  add column if not exists skill_name text
    check (
      skill_name is null
      or skill_name ~ '^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$'
    ),
  add column if not exists skill_version text
    check (
      skill_version is null
      or (
        length(skill_version) between 5 and 80
        and skill_version ~ '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
      )
    );

-- Skill identity is server-owned. Direct runtime table writes are revoked;
-- this trigger remains defense-in-depth for any future role grant. Approved
-- SECURITY DEFINER open/upgrade/adoption RPCs execute as the table owner.
create or replace function public.guard_agentic_rectification_case_skill_identity()
returns trigger
language plpgsql
set search_path = ''
as $$
declare
  v_table_owner name;
begin
  if new.skill_name is not distinct from old.skill_name
    and new.skill_version is not distinct from old.skill_version
    and new.skill_sha256 is not distinct from old.skill_sha256
    and new.skill_source_commit is not distinct from old.skill_source_commit then
    return new;
  end if;

  select pg_catalog.pg_get_userbyid(c.relowner)
  into v_table_owner
  from pg_catalog.pg_class c
  where c.oid = tg_relid;

  if current_user <> v_table_owner then
    raise exception 'agentic_rectification_skill_identity_immutable'
      using errcode = '55000';
  end if;
  return new;
end;
$$;

revoke all on function public.guard_agentic_rectification_case_skill_identity()
  from public, anon, authenticated, service_role;

drop trigger if exists agentic_rectification_case_skill_identity_guard
  on public.agentic_rectification_cases;
create trigger agentic_rectification_case_skill_identity_guard
before update of skill_name, skill_version, skill_sha256, skill_source_commit
  on public.agentic_rectification_cases
for each row execute function public.guard_agentic_rectification_case_skill_identity();

create table if not exists public.agentic_rectification_skill_upgrade_receipts (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  user_id uuid not null references auth.users(id) on delete cascade,
  previous_skill_name text not null,
  previous_skill_version text not null,
  previous_skill_sha256 text check (previous_skill_sha256 is null or previous_skill_sha256 ~ '^[0-9a-f]{64}$'),
  previous_source_commit text check (previous_source_commit is null or previous_source_commit ~ '^[0-9a-f]{40}$'),
  previous_identity_status text not null default 'verified',
  upgrade_kind text not null default 'version_upgrade',
  skill_name text not null,
  skill_version text not null,
  skill_sha256 text not null check (skill_sha256 ~ '^[0-9a-f]{64}$'),
  source_commit text check (source_commit is null or source_commit ~ '^[0-9a-f]{40}$'),
  created_at timestamptz not null default pg_catalog.now()
);

create index if not exists agentic_rectification_skill_upgrade_receipts_case_idx
  on public.agentic_rectification_skill_upgrade_receipts (case_id, created_at desc);

-- A prior local draft required a verified previous hash. Legacy Cases predate
-- immutable package receipts, so adoption records must preserve a NULL previous
-- hash together with an explicit legacy_unverifiable status.
alter table public.agentic_rectification_skill_upgrade_receipts
  alter column previous_skill_sha256 drop not null,
  add column if not exists previous_identity_status text not null default 'verified',
  add column if not exists upgrade_kind text not null default 'version_upgrade';

alter table public.agentic_rectification_skill_upgrade_receipts
  owner to current_user,
  drop constraint if exists agentic_rectification_skill_upgrade_receipts_previous_identity_status_check,
  add constraint agentic_rectification_skill_upgrade_receipts_previous_identity_status_check
    check (previous_identity_status in ('verified', 'legacy_unverifiable')),
  drop constraint if exists agentic_rectification_skill_upgrade_receipts_upgrade_kind_check,
  add constraint agentic_rectification_skill_upgrade_receipts_upgrade_kind_check
    check (upgrade_kind in ('version_upgrade', 'legacy_adoption'));

drop index if exists public.agentic_rectification_skill_upgrade_receipts_legacy_adoption_idx;
create unique index agentic_rectification_skill_upgrade_receipts_legacy_adoption_idx
  on public.agentic_rectification_skill_upgrade_receipts (case_id)
  where upgrade_kind = 'legacy_adoption';

alter table public.agentic_rectification_skill_upgrade_receipts enable row level security;
revoke all on table public.agentic_rectification_skill_upgrade_receipts
  from public, anon, authenticated, service_role;

create or replace function public.reject_agentic_rectification_skill_receipt_mutation()
returns trigger
language plpgsql
set search_path = ''
as $$
begin
  if tg_op = 'TRUNCATE' then
    raise exception 'agentic_rectification_skill_receipts_are_append_only'
      using errcode = '55000';
  end if;

  if tg_op = 'UPDATE' then
    raise exception 'agentic_rectification_skill_receipts_are_append_only'
      using errcode = '55000';
  end if;

  -- Direct receipt deletion is forbidden while every referenced parent still
  -- exists. PostgreSQL FK cascades run after the parent row is deleted, so an
  -- account/Case/Turn lifecycle purge is allowed without a mutable session flag
  -- or a runtime-role backdoor.
  if tg_table_name = 'agentic_rectification_skill_upgrade_receipts' then
    if exists (
      select 1 from public.agentic_rectification_cases where id = old.case_id
    ) and exists (
      select 1 from auth.users where id = old.user_id
    ) then
      raise exception 'agentic_rectification_skill_receipts_are_append_only'
        using errcode = '55000';
    end if;
  elsif tg_table_name = 'agentic_rectification_skill_run_receipts' then
    if exists (
      select 1 from public.agentic_rectification_cases where id = old.case_id
    ) and exists (
      select 1 from public.agentic_rectification_turns where id = old.turn_id
    ) and exists (
      select 1 from auth.users where id = old.user_id
    ) then
      raise exception 'agentic_rectification_skill_receipts_are_append_only'
        using errcode = '55000';
    end if;
  else
    raise exception 'agentic_rectification_skill_receipts_are_append_only'
      using errcode = '55000';
  end if;

  return old;
end;
$$;

revoke all on function public.reject_agentic_rectification_skill_receipt_mutation()
  from public, anon, authenticated, service_role;

drop trigger if exists agentic_rectification_skill_upgrade_receipts_append_only
  on public.agentic_rectification_skill_upgrade_receipts;
create trigger agentic_rectification_skill_upgrade_receipts_append_only
before update or delete on public.agentic_rectification_skill_upgrade_receipts
for each row execute function public.reject_agentic_rectification_skill_receipt_mutation();
drop trigger if exists agentic_rectification_skill_upgrade_receipts_truncate_guard
  on public.agentic_rectification_skill_upgrade_receipts;
create trigger agentic_rectification_skill_upgrade_receipts_truncate_guard
before truncate on public.agentic_rectification_skill_upgrade_receipts
for each statement execute function public.reject_agentic_rectification_skill_receipt_mutation();

-- On repeat apply, CREATE OR REPLACE FUNCTION requires the migration owner to
-- retain EXECUTE on an existing function. Grant it only for the duration of this
-- migration; the reconciliation block below removes it again, leaving service_role
-- as the only runtime EXECUTE grant.
do $migration$
declare
  v_function regprocedure;
  v_signature text;
begin
  foreach v_signature in array array[
    'public.open_agentic_rectification_case_v2(uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb)',
    'public.get_agentic_rectification_skill_identity(uuid, uuid)',
    'public.get_agentic_rectification_skill_identity_status(uuid, uuid)',
    'public.upgrade_agentic_rectification_skill_v2(uuid, uuid, text, text, text, text)',
    'public.adopt_agentic_rectification_skill_v1(uuid, uuid, text, text, text, text)',
    'public.insert_agentic_rectification_skill_run_receipt(uuid, uuid, uuid, uuid, text, text, text, text, text)'
  ] loop
    v_function := pg_catalog.to_regprocedure(v_signature);
    if v_function is not null then
      execute pg_catalog.format(
        'grant execute on function %s to %I',
        v_function,
        current_user
      );
    end if;
  end loop;
end
$migration$;

create or replace function public.open_agentic_rectification_case_v2(
  p_user_id uuid,
  p_request_id uuid,
  p_intent text,
  p_session_id uuid,
  p_skill_name text,
  p_skill_version text,
  p_skill_sha256 text,
  p_skill_source_commit text,
  p_baseline_profile_fingerprint text,
  p_baseline_birth_snapshot jsonb,
  p_candidate_range jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_result jsonb;
  v_case public.agentic_rectification_cases%rowtype;
  v_case_id uuid;
  v_is_new boolean;
begin
  if p_skill_name is null or length(btrim(p_skill_name)) = 0
    or p_skill_version is null or length(btrim(p_skill_version)) = 0
    or p_skill_sha256 is null or p_skill_sha256 !~ '^[0-9a-f]{64}$'
    or (p_skill_source_commit is not null and p_skill_source_commit !~ '^[0-9a-f]{40}$') then
    raise exception 'agentic_rectification_invalid_skill_identity' using errcode = 'P0001';
  end if;

  v_result := public.open_agentic_rectification_case(
    p_user_id,
    p_request_id,
    p_intent,
    p_session_id,
    p_skill_name,
    p_skill_version,
    p_baseline_profile_fingerprint,
    p_baseline_birth_snapshot,
    p_candidate_range
  );
  v_case_id := (v_result ->> 'case_id')::uuid;
  v_is_new := coalesce((v_result ->> 'should_start_opening')::boolean, false);

  select * into v_case
  from public.agentic_rectification_cases
  where id = v_case_id and user_id = p_user_id
  for update;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;

  if v_case.skill_sha256 is null then
    if not v_is_new then
      raise exception 'agentic_rectification_skill_identity_missing' using errcode = 'P0001';
    end if;
    update public.agentic_rectification_cases
    set skill_sha256 = p_skill_sha256,
        skill_source_commit = p_skill_source_commit
    where id = v_case.id
    returning * into v_case;
  end if;

  if v_case.skill_name is distinct from p_skill_name
    or v_case.skill_version is distinct from p_skill_version
    or v_case.skill_sha256 is distinct from p_skill_sha256
    or v_case.skill_source_commit is distinct from p_skill_source_commit then
    raise exception 'agentic_rectification_skill_identity_mismatch' using errcode = 'P0001';
  end if;

  return v_result || jsonb_build_object(
    'skill_name', v_case.skill_name,
    'skill_version', v_case.skill_version,
    'skill_sha256', v_case.skill_sha256,
    'skill_source_commit', v_case.skill_source_commit
  );
end;
$$;

revoke all on function public.open_agentic_rectification_case_v2(
  uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb
) from public, anon, authenticated, service_role;
alter function public.open_agentic_rectification_case_v2(
  uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb
) owner to current_user;
grant execute on function public.open_agentic_rectification_case_v2(
  uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb
) to service_role;

create or replace function public.get_agentic_rectification_skill_identity(
  p_user_id uuid,
  p_case_id uuid
)
returns jsonb
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
begin
  select * into v_case
  from public.agentic_rectification_cases
  where id = p_case_id and user_id = p_user_id;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;
  if v_case.skill_sha256 is null then
    raise exception 'agentic_rectification_legacy_skill_identity_unverifiable' using errcode = 'P0001';
  end if;
  return jsonb_build_object(
    'skill_name', v_case.skill_name,
    'skill_version', v_case.skill_version,
    'skill_sha256', v_case.skill_sha256,
    'skill_source_commit', v_case.skill_source_commit
  );
end;
$$;

revoke all on function public.get_agentic_rectification_skill_identity(uuid, uuid)
  from public, anon, authenticated, service_role;
alter function public.get_agentic_rectification_skill_identity(uuid, uuid)
  owner to current_user;
grant execute on function public.get_agentic_rectification_skill_identity(uuid, uuid)
  to service_role;

create or replace function public.get_agentic_rectification_skill_identity_status(
  p_user_id uuid,
  p_case_id uuid
)
returns jsonb
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
begin
  select * into v_case
  from public.agentic_rectification_cases
  where id = p_case_id and user_id = p_user_id;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;

  return jsonb_build_object(
    'skill_name', v_case.skill_name,
    'skill_version', v_case.skill_version,
    'skill_sha256', v_case.skill_sha256,
    'skill_source_commit', v_case.skill_source_commit,
    'skill_identity_status', case
      when v_case.skill_sha256 is null then 'legacy_unverifiable'
      else 'verified'
    end,
    'requires_skill_adoption',
      v_case.skill_sha256 is null
      and v_case.status = any (public.agentic_rectification_resumable_statuses())
  );
end;
$$;

revoke all on function public.get_agentic_rectification_skill_identity_status(uuid, uuid)
  from public, anon, authenticated, service_role;
alter function public.get_agentic_rectification_skill_identity_status(uuid, uuid)
  owner to current_user;
grant execute on function public.get_agentic_rectification_skill_identity_status(uuid, uuid)
  to service_role;

create or replace function public.upgrade_agentic_rectification_skill_v2(
  p_user_id uuid,
  p_case_id uuid,
  p_skill_name text,
  p_skill_version text,
  p_skill_sha256 text,
  p_skill_source_commit text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_receipt_id uuid;
begin
  if p_skill_name is null or length(btrim(p_skill_name)) = 0
    or p_skill_version is null or length(btrim(p_skill_version)) = 0
    or p_skill_sha256 is null or p_skill_sha256 !~ '^[0-9a-f]{64}$'
    or (p_skill_source_commit is not null and p_skill_source_commit !~ '^[0-9a-f]{40}$') then
    raise exception 'agentic_rectification_invalid_skill_identity' using errcode = 'P0001';
  end if;

  select * into v_case
  from public.agentic_rectification_cases
  where id = p_case_id and user_id = p_user_id
  for update;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;
  if not (
    v_case.status = any (public.agentic_rectification_resumable_statuses())
  ) then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;
  if v_case.skill_sha256 is null then
    raise exception 'agentic_rectification_skill_identity_missing' using errcode = 'P0001';
  end if;
  if v_case.skill_name is distinct from p_skill_name then
    raise exception 'agentic_rectification_skill_name_mismatch' using errcode = 'P0001';
  end if;

  if v_case.skill_version = p_skill_version
    and v_case.skill_sha256 = p_skill_sha256
    and v_case.skill_source_commit is not distinct from p_skill_source_commit then
    return jsonb_build_object(
      'success', true,
      'case_id', v_case.id,
      'previous_skill_name', v_case.skill_name,
      'previous_skill_version', v_case.skill_version,
      'previous_skill_sha256', v_case.skill_sha256,
      'previous_source_commit', v_case.skill_source_commit,
      'skill_name', v_case.skill_name,
      'skill_version', v_case.skill_version,
      'skill_sha256', v_case.skill_sha256,
      'source_commit', v_case.skill_source_commit,
      'receipt_id', null,
      'idempotent', true
    );
  end if;

  insert into public.agentic_rectification_skill_upgrade_receipts (
    case_id, user_id,
    previous_skill_name, previous_skill_version, previous_skill_sha256, previous_source_commit,
    previous_identity_status, upgrade_kind,
    skill_name, skill_version, skill_sha256, source_commit
  ) values (
    v_case.id, p_user_id,
    v_case.skill_name, v_case.skill_version, v_case.skill_sha256, v_case.skill_source_commit,
    'verified', 'version_upgrade',
    p_skill_name, p_skill_version, p_skill_sha256, p_skill_source_commit
  ) returning id into v_receipt_id;

  update public.agentic_rectification_cases
  set skill_version = p_skill_version,
      skill_sha256 = p_skill_sha256,
      skill_source_commit = p_skill_source_commit,
      updated_at = pg_catalog.now(),
      last_activity_at = pg_catalog.now()
  where id = v_case.id;

  return jsonb_build_object(
    'success', true,
    'case_id', v_case.id,
    'previous_skill_name', v_case.skill_name,
    'previous_skill_version', v_case.skill_version,
    'previous_skill_sha256', v_case.skill_sha256,
    'previous_source_commit', v_case.skill_source_commit,
    'skill_name', p_skill_name,
    'skill_version', p_skill_version,
    'skill_sha256', p_skill_sha256,
    'source_commit', p_skill_source_commit,
    'receipt_id', v_receipt_id,
    'idempotent', false
  );
end;
$$;

revoke all on function public.upgrade_agentic_rectification_skill_v2(
  uuid, uuid, text, text, text, text
) from public, anon, authenticated, service_role;
alter function public.upgrade_agentic_rectification_skill_v2(
  uuid, uuid, text, text, text, text
) owner to current_user;
grant execute on function public.upgrade_agentic_rectification_skill_v2(
  uuid, uuid, text, text, text, text
) to service_role;

create or replace function public.adopt_agentic_rectification_skill_v1(
  p_user_id uuid,
  p_case_id uuid,
  p_skill_name text,
  p_skill_version text,
  p_skill_sha256 text,
  p_skill_source_commit text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_receipt public.agentic_rectification_skill_upgrade_receipts%rowtype;
begin
  if p_skill_name is null or length(btrim(p_skill_name)) = 0
    or p_skill_version is null or length(btrim(p_skill_version)) = 0
    or p_skill_sha256 is null or p_skill_sha256 !~ '^[0-9a-f]{64}$'
    or (p_skill_source_commit is not null and p_skill_source_commit !~ '^[0-9a-f]{40}$') then
    raise exception 'agentic_rectification_invalid_skill_identity' using errcode = 'P0001';
  end if;

  select * into v_case
  from public.agentic_rectification_cases
  where id = p_case_id and user_id = p_user_id
  for update;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;
  if not (
    v_case.status = any (public.agentic_rectification_resumable_statuses())
  ) then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;
  if v_case.skill_name is distinct from p_skill_name then
    raise exception 'agentic_rectification_skill_name_mismatch' using errcode = 'P0001';
  end if;

  if v_case.skill_sha256 is not null then
    select * into v_receipt
    from public.agentic_rectification_skill_upgrade_receipts
    where case_id = v_case.id
      and upgrade_kind = 'legacy_adoption'
      and skill_name = p_skill_name
      and skill_version = p_skill_version
      and skill_sha256 = p_skill_sha256
      and source_commit is not distinct from p_skill_source_commit
    order by created_at desc
    limit 1;

    if found
      and v_case.skill_version = p_skill_version
      and v_case.skill_sha256 = p_skill_sha256
      and v_case.skill_source_commit is not distinct from p_skill_source_commit then
      return jsonb_build_object(
        'success', true,
        'case_id', v_case.id,
        'previous_skill_name', v_receipt.previous_skill_name,
        'previous_skill_version', v_receipt.previous_skill_version,
        'previous_skill_sha256', v_receipt.previous_skill_sha256,
        'previous_source_commit', v_receipt.previous_source_commit,
        'previous_identity_status', v_receipt.previous_identity_status,
        'upgrade_kind', v_receipt.upgrade_kind,
        'skill_name', v_case.skill_name,
        'skill_version', v_case.skill_version,
        'skill_sha256', v_case.skill_sha256,
        'source_commit', v_case.skill_source_commit,
        'receipt_id', v_receipt.id,
        'idempotent', true
      );
    end if;

    raise exception 'agentic_rectification_skill_identity_already_verified' using errcode = 'P0001';
  end if;

  insert into public.agentic_rectification_skill_upgrade_receipts (
    case_id, user_id,
    previous_skill_name, previous_skill_version, previous_skill_sha256, previous_source_commit,
    previous_identity_status, upgrade_kind,
    skill_name, skill_version, skill_sha256, source_commit
  ) values (
    v_case.id, p_user_id,
    v_case.skill_name, v_case.skill_version, null, v_case.skill_source_commit,
    'legacy_unverifiable', 'legacy_adoption',
    p_skill_name, p_skill_version, p_skill_sha256, p_skill_source_commit
  ) returning * into v_receipt;

  update public.agentic_rectification_cases
  set skill_version = p_skill_version,
      skill_sha256 = p_skill_sha256,
      skill_source_commit = p_skill_source_commit,
      updated_at = pg_catalog.now(),
      last_activity_at = pg_catalog.now()
  where id = v_case.id;

  return jsonb_build_object(
    'success', true,
    'case_id', v_case.id,
    'previous_skill_name', v_case.skill_name,
    'previous_skill_version', v_case.skill_version,
    'previous_skill_sha256', null,
    'previous_source_commit', v_case.skill_source_commit,
    'previous_identity_status', 'legacy_unverifiable',
    'upgrade_kind', 'legacy_adoption',
    'skill_name', p_skill_name,
    'skill_version', p_skill_version,
    'skill_sha256', p_skill_sha256,
    'source_commit', p_skill_source_commit,
    'receipt_id', v_receipt.id,
    'idempotent', false
  );
end;
$$;

revoke all on function public.adopt_agentic_rectification_skill_v1(
  uuid, uuid, text, text, text, text
) from public, anon, authenticated, service_role;
alter function public.adopt_agentic_rectification_skill_v1(
  uuid, uuid, text, text, text, text
) owner to current_user;
grant execute on function public.adopt_agentic_rectification_skill_v1(
  uuid, uuid, text, text, text, text
) to service_role;

create table if not exists public.agentic_rectification_skill_run_receipts (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  turn_id uuid not null references public.agentic_rectification_turns(id) on delete cascade,
  request_id uuid not null,
  run_kind text not null,
  user_id uuid not null references auth.users(id) on delete cascade,
  skill_name text not null,
  skill_version text not null,
  skill_sha256 text not null check (skill_sha256 ~ '^[0-9a-f]{64}$'),
  source_commit text check (source_commit is null or source_commit ~ '^[0-9a-f]{40}$'),
  loaded_at timestamptz not null default pg_catalog.now()
);

-- Keep repeat execution safe even if the table was created by a prior local
-- draft before request-scoped immutable receipts were added.
alter table public.agentic_rectification_skill_run_receipts
  add column if not exists request_id uuid,
  add column if not exists run_kind text;

update public.agentic_rectification_skill_run_receipts
set request_id = id
where request_id is null;

update public.agentic_rectification_skill_run_receipts
set run_kind = 'turn'
where run_kind is null;

alter table public.agentic_rectification_skill_run_receipts
  owner to current_user,
  alter column request_id set not null,
  alter column run_kind set not null,
  drop constraint if exists agentic_rectification_skill_run_receipts_run_kind_check,
  add constraint agentic_rectification_skill_run_receipts_run_kind_check
    check (run_kind in ('turn', 'regeneration'));

-- A prior draft used UNIQUE(turn_id), which prevents a regeneration from
-- recording a second immutable receipt for the same Turn. Remove only unique
-- constraints/indexes whose complete key is exactly turn_id.
do $migration$
declare
  v_name name;
begin
  for v_name in
    select c.conname
    from pg_catalog.pg_constraint c
    where c.conrelid = 'public.agentic_rectification_skill_run_receipts'::regclass
      and c.contype = 'u'
      and (
        select pg_catalog.array_agg(a.attname::text order by k.ordinality)
        from pg_catalog.unnest(c.conkey) with ordinality as k(attnum, ordinality)
        join pg_catalog.pg_attribute a
          on a.attrelid = c.conrelid and a.attnum = k.attnum
      ) = array['turn_id']::text[]
  loop
    execute pg_catalog.format(
      'alter table public.agentic_rectification_skill_run_receipts drop constraint %I',
      v_name
    );
  end loop;

  for v_name in
    select ci.relname
    from pg_catalog.pg_index i
    join pg_catalog.pg_class ci on ci.oid = i.indexrelid
    where i.indrelid = 'public.agentic_rectification_skill_run_receipts'::regclass
      and i.indisunique
      and not i.indisprimary
      and not exists (
        select 1 from pg_catalog.pg_constraint c where c.conindid = i.indexrelid
      )
      and (
        select pg_catalog.array_agg(a.attname::text order by k.ordinality)
        from pg_catalog.unnest(i.indkey) with ordinality as k(attnum, ordinality)
        join pg_catalog.pg_attribute a
          on a.attrelid = i.indrelid and a.attnum = k.attnum
        where k.attnum > 0
      ) = array['turn_id']::text[]
  loop
    execute pg_catalog.format('drop index public.%I', v_name);
  end loop;
end;
$migration$;

drop index if exists public.agentic_rectification_skill_run_receipts_request_idx;
create unique index agentic_rectification_skill_run_receipts_request_idx
  on public.agentic_rectification_skill_run_receipts (
    case_id, turn_id, request_id, run_kind
  );

create index if not exists agentic_rectification_skill_run_receipts_case_idx
  on public.agentic_rectification_skill_run_receipts (case_id, loaded_at desc);

alter table public.agentic_rectification_skill_run_receipts enable row level security;
revoke all on table public.agentic_rectification_skill_run_receipts
  from public, anon, authenticated, service_role;

drop trigger if exists agentic_rectification_skill_run_receipts_append_only
  on public.agentic_rectification_skill_run_receipts;
create trigger agentic_rectification_skill_run_receipts_append_only
before update or delete on public.agentic_rectification_skill_run_receipts
for each row execute function public.reject_agentic_rectification_skill_receipt_mutation();
drop trigger if exists agentic_rectification_skill_run_receipts_truncate_guard
  on public.agentic_rectification_skill_run_receipts;
create trigger agentic_rectification_skill_run_receipts_truncate_guard
before truncate on public.agentic_rectification_skill_run_receipts
for each statement execute function public.reject_agentic_rectification_skill_receipt_mutation();

create or replace function public.insert_agentic_rectification_skill_run_receipt(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid,
  p_request_id uuid,
  p_run_kind text,
  p_skill_name text,
  p_skill_version text,
  p_skill_sha256 text,
  p_source_commit text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_receipt public.agentic_rectification_skill_run_receipts%rowtype;
begin
  if p_request_id is null or p_run_kind is null
    or p_run_kind not in ('turn', 'regeneration') then
    raise exception 'agentic_rectification_invalid_skill_receipt' using errcode = 'P0001';
  end if;
  select * into v_case
  from public.agentic_rectification_cases
  where id = p_case_id and user_id = p_user_id
  for update;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;
  if not exists (
    select 1 from public.agentic_rectification_turns
    where id = p_turn_id and case_id = p_case_id
  ) then
    raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
  end if;
  if v_case.skill_name is distinct from p_skill_name
    or v_case.skill_version is distinct from p_skill_version
    or v_case.skill_sha256 is distinct from p_skill_sha256
    or v_case.skill_source_commit is distinct from p_source_commit then
    raise exception 'agentic_rectification_skill_identity_mismatch' using errcode = 'P0001';
  end if;

  insert into public.agentic_rectification_skill_run_receipts (
    case_id, turn_id, request_id, run_kind, user_id,
    skill_name, skill_version, skill_sha256, source_commit
  ) values (
    p_case_id, p_turn_id, p_request_id, p_run_kind, p_user_id,
    p_skill_name, p_skill_version, p_skill_sha256, p_source_commit
  )
  on conflict (case_id, turn_id, request_id, run_kind) do nothing
  returning * into v_receipt;

  if v_receipt.id is null then
    select * into v_receipt
    from public.agentic_rectification_skill_run_receipts
    where case_id = p_case_id
      and turn_id = p_turn_id
      and request_id = p_request_id
      and run_kind = p_run_kind;

    if not found
      or v_receipt.user_id is distinct from p_user_id
      or v_receipt.skill_name is distinct from p_skill_name
      or v_receipt.skill_version is distinct from p_skill_version
      or v_receipt.skill_sha256 is distinct from p_skill_sha256
      or v_receipt.source_commit is distinct from p_source_commit then
      raise exception 'agentic_rectification_skill_receipt_conflict' using errcode = 'P0001';
    end if;
  end if;

  return jsonb_build_object(
    'receipt_id', v_receipt.id,
    'request_id', v_receipt.request_id,
    'run_kind', v_receipt.run_kind,
    'skill_name', v_receipt.skill_name,
    'skill_version', v_receipt.skill_version,
    'skill_sha256', v_receipt.skill_sha256,
    'source_commit', v_receipt.source_commit
  );
end;
$$;

revoke all on function public.insert_agentic_rectification_skill_run_receipt(
  uuid, uuid, uuid, uuid, text, text, text, text, text
) from public, anon, authenticated, service_role;
alter function public.insert_agentic_rectification_skill_run_receipt(
  uuid, uuid, uuid, uuid, text, text, text, text, text
) owner to current_user;
grant execute on function public.insert_agentic_rectification_skill_run_receipt(
  uuid, uuid, uuid, uuid, text, text, text, text, text
) to service_role;

-- CREATE OR REPLACE FUNCTION preserves existing explicit ACL entries. Reconcile
-- historical grants on every SECURITY DEFINER RPC exposed by this migration,
-- retaining only the intended service_role grant. Revoking a direct grant from
-- service_runtime does not alter its GRANT service_role membership, so the runtime
-- path that SET ROLE service_role retains effective EXECUTE.
do $migration$
declare
  v_service_role oid;
  v_function record;
  v_grantee name;
  v_function_identity text;
begin
  select r.oid
    into v_service_role
  from pg_catalog.pg_roles r
  where r.rolname = 'service_role';

  if v_service_role is null then
    raise exception 'immutable_skill_registry_service_role_missing';
  end if;

  for v_function in
    select p.oid, p.proname,
      pg_catalog.pg_get_function_identity_arguments(p.oid) as identity_arguments
    from pg_catalog.pg_proc p
    join pg_catalog.pg_namespace n on n.oid = p.pronamespace
    where n.nspname = 'public'
      and p.prosecdef
      and p.oid in (
        'public.open_agentic_rectification_case_v2(uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb)'::regprocedure,
        'public.get_agentic_rectification_skill_identity(uuid, uuid)'::regprocedure,
        'public.get_agentic_rectification_skill_identity_status(uuid, uuid)'::regprocedure,
        'public.upgrade_agentic_rectification_skill_v2(uuid, uuid, text, text, text, text)'::regprocedure,
        'public.adopt_agentic_rectification_skill_v1(uuid, uuid, text, text, text, text)'::regprocedure,
        'public.insert_agentic_rectification_skill_run_receipt(uuid, uuid, uuid, uuid, text, text, text, text, text)'::regprocedure
      )
  loop
    v_function_identity := pg_catalog.format(
      'public.%I(%s)',
      v_function.proname,
      v_function.identity_arguments
    );

    -- PUBLIC is represented by grantee OID 0 and must use the PUBLIC
    -- keyword, not a quoted role named "public".
    execute pg_catalog.format(
      'revoke execute on function %s from public',
      v_function_identity
    );

    for v_grantee in
      select distinct pg_catalog.pg_get_userbyid(a.grantee)::name
      from pg_catalog.pg_proc p
      cross join lateral pg_catalog.aclexplode(p.proacl) a
      where p.oid = v_function.oid
        and a.privilege_type = 'EXECUTE'
        and a.grantee <> 0
        and a.grantee <> v_service_role
    loop
      execute pg_catalog.format(
        'revoke execute on function %s from %I',
        v_function_identity,
        v_grantee
      );
    end loop;
  end loop;
end
$migration$;
