-- Rectification V10 durable conversation/runtime contract.
-- Additive only: V9 RPC signatures remain available for older runners.

begin;

-- ---------------------------------------------------------------------------
-- 1. Durable run attempts and receipt ownership
-- ---------------------------------------------------------------------------

create table if not exists public.agentic_rectification_run_attempts (
  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,
  attempt_number integer not null check (attempt_number > 0),
  status text not null default 'started' check (
    status in ('started', 'completed', 'failed', 'retryable', 'aborted')
  ),
  error_code text,
  usage jsonb not null default '{}'::jsonb check (jsonb_typeof(usage) = 'object'),
  started_at timestamptz not null default pg_catalog.now(),
  completed_at timestamptz,
  updated_at timestamptz not null default pg_catalog.now(),
  unique (turn_id, attempt_number),
  unique (id, case_id, turn_id),
  check (status = 'started' or completed_at is not null)
);

create index if not exists agentic_rectification_run_attempts_turn_idx
  on public.agentic_rectification_run_attempts (case_id, turn_id, attempt_number desc);
create index if not exists agentic_rectification_run_attempts_success_idx
  on public.agentic_rectification_run_attempts (turn_id, completed_at desc)
  where status = 'completed';

alter table public.agentic_rectification_run_phases
  add column if not exists attempt_id uuid references public.agentic_rectification_run_attempts(id) on delete cascade;
alter table public.agentic_rectification_tool_receipts
  add column if not exists attempt_id uuid references public.agentic_rectification_run_attempts(id) on delete cascade;
alter table public.agentic_rectification_turns
  add column if not exists successful_attempt_id uuid references public.agentic_rectification_run_attempts(id) on delete set null;
alter table public.agentic_rectification_turns
  add column if not exists request_id uuid;
create unique index if not exists agentic_rectification_turns_case_request_idx
  on public.agentic_rectification_turns (case_id, request_id)
  where request_id is not null;

-- Keep the storage-layer allowlists aligned with the V10 RPC allowlists. The
-- original V9 CHECK constraints would otherwise reject valid V10 receipts.
alter table public.agentic_rectification_run_phases
  drop constraint if exists agentic_rectification_run_phases_phase_check,
  add constraint agentic_rectification_run_phases_phase_check check (
    phase in (
      'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
      'intent.classified', 'evidence.proposed', 'evidence.confirmed',
      'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
      'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
      'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
    )
  );

alter table public.agentic_rectification_tool_receipts
  drop constraint if exists agentic_rectification_tool_receipts_tool_name_check,
  add constraint agentic_rectification_tool_receipts_tool_name_check check (
    tool_name in (
      'rectification-read-case', 'rectification-set-focus',
      'rectification-resolve-focus', 'rectification-record-evidence-batch',
      'rectification-propose-evidence', 'rectification-confirm-evidence',
      'rectification-revise-evidence', 'rectification-compare-candidates',
      'rectification-read-diagnostics', 'rectification-offer-candidates',
      'rectification-accept-candidate', 'rectification-confirm-birth-time',
      'rectification-close-case'
    )
  ),
  drop constraint if exists agentic_rectification_tool_receipts_public_phase_check,
  add constraint agentic_rectification_tool_receipts_public_phase_check check (
    public_phase in (
      'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
      'intent.classified', 'evidence.proposed', 'evidence.confirmed',
      'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
      'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
      'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
    )
  );

-- Enforce Case/Turn/Attempt scope below the SECURITY DEFINER RPC layer.
alter table public.agentic_rectification_turns
  drop constraint if exists agentic_rectification_turns_id_case_id_key,
  add constraint agentic_rectification_turns_id_case_id_key unique (id, case_id);
alter table public.agentic_rectification_run_attempts
  drop constraint if exists agentic_rectification_run_attempts_turn_case_fkey,
  add constraint agentic_rectification_run_attempts_turn_case_fkey
    foreign key (turn_id, case_id)
    references public.agentic_rectification_turns(id, case_id) on delete cascade;
alter table public.agentic_rectification_run_phases
  drop constraint if exists agentic_rectification_run_phases_attempt_scope_fkey,
  add constraint agentic_rectification_run_phases_attempt_scope_fkey
    foreign key (attempt_id, case_id, turn_id)
    references public.agentic_rectification_run_attempts(id, case_id, turn_id) on delete cascade;
alter table public.agentic_rectification_tool_receipts
  drop constraint if exists agentic_rectification_tool_receipts_attempt_scope_fkey,
  add constraint agentic_rectification_tool_receipts_attempt_scope_fkey
    foreign key (attempt_id, case_id, turn_id)
    references public.agentic_rectification_run_attempts(id, case_id, turn_id) on delete cascade;

create index if not exists agentic_rectification_run_phases_attempt_idx
  on public.agentic_rectification_run_phases (attempt_id, sequence);
create index if not exists agentic_rectification_tool_receipts_attempt_idx
  on public.agentic_rectification_tool_receipts (attempt_id, started_at);

-- ---------------------------------------------------------------------------
-- 2. ConversationFocus and server-owned CaseConversationSummary
-- ---------------------------------------------------------------------------

create table if not exists public.agentic_rectification_conversation_focuses (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  question_id text not null check (length(btrim(question_id)) > 0),
  intent text not null check (length(btrim(intent)) > 0),
  target_evidence_id uuid references public.agentic_rectification_evidence(id) on delete set null,
  target_domain text check (
    target_domain is null or target_domain in (
      'education', 'career', 'relationship', 'relocation', 'finance', 'health', 'family', 'other'
    )
  ),
  target_kind text check (
    target_kind is null or target_kind in (
      'education_start', 'education_completion', 'education_interruption',
      'career_entry', 'career_change', 'promotion', 'career_pressure', 'career_exit',
      'relationship_start', 'relationship_commitment', 'relationship_separation',
      'relocation', 'finance_gain', 'finance_loss',
      'self_health_event', 'family_event', 'other'
    )
  ),
  expected_answer_schema jsonb not null default '{}'::jsonb
    check (jsonb_typeof(expected_answer_schema) = 'object'),
  status text not null default 'active' check (
    status in ('active', 'resolved', 'declined', 'skipped', 'superseded')
  ),
  asked_at timestamptz not null default pg_catalog.now(),
  resolved_at timestamptz,
  created_at timestamptz not null default pg_catalog.now(),
  updated_at timestamptz not null default pg_catalog.now(),
  unique (case_id, question_id),
  check (
    (status = 'active' and resolved_at is null)
    or (status <> 'active' and resolved_at is not null)
  )
);

create unique index if not exists agentic_rectification_one_active_focus_per_case
  on public.agentic_rectification_conversation_focuses (case_id)
  where status = 'active';
create index if not exists agentic_rectification_focus_case_history_idx
  on public.agentic_rectification_conversation_focuses (case_id, asked_at desc);
create index if not exists agentic_rectification_focus_evidence_idx
  on public.agentic_rectification_conversation_focuses (target_evidence_id)
  where target_evidence_id is not null;

create table if not exists public.agentic_rectification_case_conversation_summaries (
  case_id uuid primary key references public.agentic_rectification_cases(id) on delete cascade,
  confirmed_evidence_summary jsonb not null default '[]'::jsonb
    check (jsonb_typeof(confirmed_evidence_summary) = 'array'),
  pending_revisions jsonb not null default '[]'::jsonb
    check (jsonb_typeof(pending_revisions) = 'array'),
  active_focus jsonb,
  declined_skipped_topics jsonb not null default '[]'::jsonb
    check (jsonb_typeof(declined_skipped_topics) = 'array'),
  candidate_divergence_summary jsonb,
  missing_evidence_categories jsonb not null default '[]'::jsonb
    check (jsonb_typeof(missing_evidence_categories) = 'array'),
  last_result_policy jsonb,
  summary_version integer not null default 1 check (summary_version > 0),
  updated_at timestamptz not null default pg_catalog.now(),
  check (active_focus is null or jsonb_typeof(active_focus) = 'object'),
  check (candidate_divergence_summary is null or jsonb_typeof(candidate_divergence_summary) = 'object'),
  check (last_result_policy is null or jsonb_typeof(last_result_policy) = 'object')
);

-- Batch idempotency is per Case and per independently parsed event.
alter table public.agentic_rectification_evidence
  add column if not exists idempotency_key text;
create unique index if not exists agentic_rectification_evidence_idempotency_idx
  on public.agentic_rectification_evidence (case_id, idempotency_key)
  where idempotency_key is not null;

-- ---------------------------------------------------------------------------
-- 3. RLS and grants: service role owns writes; browser receives projections.
-- ---------------------------------------------------------------------------

alter table public.agentic_rectification_run_attempts enable row level security;
alter table public.agentic_rectification_conversation_focuses enable row level security;
alter table public.agentic_rectification_case_conversation_summaries enable row level security;

revoke all on table public.agentic_rectification_run_attempts from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_conversation_focuses from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_case_conversation_summaries from public, anon, authenticated, service_role;

-- No direct table DML is granted. Runtime access is only through the scoped
-- SECURITY DEFINER RPCs below. Revoke inherited V9 DML on receipt tables too;
-- their legacy and V10 write paths are SECURITY DEFINER functions.
revoke insert, update, delete, truncate
  on table public.agentic_rectification_run_phases,
           public.agentic_rectification_tool_receipts
  from service_role;

-- ---------------------------------------------------------------------------
-- 4. Server summary rebuild. No raw quote, birth snapshot, score or reasoning.
-- ---------------------------------------------------------------------------

create or replace function public.refresh_agentic_rectification_case_conversation_summary(
  p_case_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_confirmed jsonb;
  v_pending jsonb;
  v_active_focus jsonb;
  v_declined_skipped jsonb;
  v_candidate_divergence jsonb;
  v_missing jsonb;
  v_last_policy jsonb;
  v_result public.agentic_rectification_results%rowtype;
  v_summary jsonb;
begin
  if p_case_id is null or not exists (
    select 1 from public.agentic_rectification_cases where id = p_case_id
  ) then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;

  select coalesce(jsonb_agg(jsonb_build_object(
    'evidence_id', e.id,
    'summary', e.summary,
    'event_kind', e.event_kind,
    'domain', e.domain,
    'date_precision', e.date_precision,
    'occurred_from', e.occurred_from,
    'occurred_to', e.occurred_to
  ) order by e.created_at, e.id), '[]'::jsonb)
  into v_confirmed
  from public.agentic_rectification_evidence e
  where e.case_id = p_case_id and e.status = 'confirmed';

  select coalesce(jsonb_agg(jsonb_build_object(
    'evidence_id', e.id,
    'supersedes_evidence_id', e.supersedes_evidence_id,
    'summary', e.summary,
    'event_kind', e.event_kind,
    'domain', e.domain,
    'date_precision', e.date_precision,
    'occurred_from', e.occurred_from,
    'occurred_to', e.occurred_to
  ) order by e.created_at, e.id), '[]'::jsonb)
  into v_pending
  from public.agentic_rectification_evidence e
  where e.case_id = p_case_id
    and e.status in ('draft', 'pending_confirmation');

  select jsonb_build_object(
    'id', f.id,
    'case_id', f.case_id,
    'question_id', f.question_id,
    'intent', f.intent,
    'target_evidence_id', f.target_evidence_id,
    'target_domain', f.target_domain,
    'target_kind', f.target_kind,
    'expected_answer_schema', f.expected_answer_schema,
    'status', f.status,
    'asked_at', f.asked_at,
    'resolved_at', f.resolved_at
  ) into v_active_focus
  from public.agentic_rectification_conversation_focuses f
  where f.case_id = p_case_id and f.status = 'active'
  order by f.asked_at desc, f.id
  limit 1;

  select coalesce(jsonb_agg(jsonb_build_object(
    'focus_id', f.id,
    'intent', f.intent,
    'target_evidence_id', f.target_evidence_id,
    'target_domain', f.target_domain,
    'target_kind', f.target_kind,
    'status', f.status,
    'asked_at', f.asked_at,
    'resolved_at', f.resolved_at
  ) order by f.resolved_at, f.id), '[]'::jsonb)
  into v_declined_skipped
  from public.agentic_rectification_conversation_focuses f
  where f.case_id = p_case_id and f.status in ('declined', 'skipped');

  select * into v_result
  from public.agentic_rectification_results
  where case_id = p_case_id and invalidated_at is null
  order by created_at desc, id desc
  limit 1;

  if v_result.id is null then
    v_candidate_divergence := null;
    v_last_policy := null;
  else
    v_candidate_divergence := jsonb_build_object(
      'result_id', v_result.id,
      'overall_confidence', v_result.overall_confidence,
      'margin_percent', v_result.margin_percent,
      'candidate_count', jsonb_array_length(coalesce(v_result.candidates, '[]'::jsonb)),
      'tied_candidate_count', (
        select count(*)
        from jsonb_array_elements(coalesce(v_result.candidates, '[]'::jsonb)) candidate
        where coalesce((candidate->>'tied_minute_count')::integer, 0) > 1
      )
    );
    v_last_policy := jsonb_build_object(
      'result_id', v_result.id,
      'selection_allowed', v_result.selection_allowed,
      'confirmation_allowed', v_result.confirmation_allowed,
      'representative_time', v_result.representative_time,
      'selected_time', v_result.selected_time,
      'selection_kind', v_result.selection_kind,
      'algorithm_version', v_result.algorithm_version
    );
  end if;

  select coalesce(jsonb_agg(domain order by ordinal), '[]'::jsonb)
  into v_missing
  from unnest(array[
    'education', 'career', 'relationship', 'relocation',
    'finance', 'health', 'family'
  ]::text[]) with ordinality as required(domain, ordinal)
  where not exists (
    select 1
    from public.agentic_rectification_evidence e
    where e.case_id = p_case_id and e.status = 'confirmed' and e.domain = required.domain
  );

  insert into public.agentic_rectification_case_conversation_summaries (
    case_id,
    confirmed_evidence_summary,
    pending_revisions,
    active_focus,
    declined_skipped_topics,
    candidate_divergence_summary,
    missing_evidence_categories,
    last_result_policy,
    summary_version,
    updated_at
  ) values (
    p_case_id,
    v_confirmed,
    v_pending,
    v_active_focus,
    v_declined_skipped,
    v_candidate_divergence,
    v_missing,
    v_last_policy,
    1,
    pg_catalog.now()
  )
  on conflict (case_id) do update set
    confirmed_evidence_summary = excluded.confirmed_evidence_summary,
    pending_revisions = excluded.pending_revisions,
    active_focus = excluded.active_focus,
    declined_skipped_topics = excluded.declined_skipped_topics,
    candidate_divergence_summary = excluded.candidate_divergence_summary,
    missing_evidence_categories = excluded.missing_evidence_categories,
    last_result_policy = excluded.last_result_policy,
    summary_version = excluded.summary_version,
    updated_at = excluded.updated_at;

  v_summary := jsonb_build_object(
    'confirmed_evidence_summary', v_confirmed,
    'pending_revisions', v_pending,
    'active_focus', v_active_focus,
    'declined_skipped_topics', v_declined_skipped,
    'candidate_divergence_summary', v_candidate_divergence,
    'missing_evidence_categories', v_missing,
    'last_result_policy', v_last_policy,
    'summary_version', 1
  );
  return v_summary;
end;
$$;

revoke all on function public.refresh_agentic_rectification_case_conversation_summary(uuid)
  from public, anon, authenticated;
grant execute on function public.refresh_agentic_rectification_case_conversation_summary(uuid)
  to service_role;

create or replace function public.agentic_rectification_refresh_conversation_summary_trigger()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_old_case_id uuid := case when tg_op in ('UPDATE', 'DELETE') then old.case_id else null end;
  v_new_case_id uuid := case when tg_op in ('INSERT', 'UPDATE') then new.case_id else null end;
begin
  if v_old_case_id is not null and exists (
    select 1 from public.agentic_rectification_cases where id = v_old_case_id
  ) then
    perform public.refresh_agentic_rectification_case_conversation_summary(v_old_case_id);
  end if;

  if v_new_case_id is not null
    and v_new_case_id is distinct from v_old_case_id
    and exists (
      select 1 from public.agentic_rectification_cases where id = v_new_case_id
    ) then
    perform public.refresh_agentic_rectification_case_conversation_summary(v_new_case_id);
  end if;

  return case when tg_op = 'DELETE' then old else new end;
end;
$$;

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

drop trigger if exists agentic_rectification_evidence_refresh_summary
  on public.agentic_rectification_evidence;
create trigger agentic_rectification_evidence_refresh_summary
after insert or update or delete on public.agentic_rectification_evidence
for each row execute function public.agentic_rectification_refresh_conversation_summary_trigger();

drop trigger if exists agentic_rectification_focus_refresh_summary
  on public.agentic_rectification_conversation_focuses;
create trigger agentic_rectification_focus_refresh_summary
after insert or update or delete on public.agentic_rectification_conversation_focuses
for each row execute function public.agentic_rectification_refresh_conversation_summary_trigger();

drop trigger if exists agentic_rectification_result_refresh_summary
  on public.agentic_rectification_results;
create trigger agentic_rectification_result_refresh_summary
after insert or update or delete on public.agentic_rectification_results
for each row execute function public.agentic_rectification_refresh_conversation_summary_trigger();

-- Terminal cases cannot retain an active question. Evidence/focus RPCs below
-- also reject all new writes once the Case is terminal.
create or replace function public.agentic_rectification_terminal_focus_guard()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  if new.status in ('confirmed', 'closed', 'abandoned', 'superseded')
    and old.status is distinct from new.status then
    update public.agentic_rectification_conversation_focuses
    set status = 'superseded',
        resolved_at = coalesce(resolved_at, pg_catalog.now()),
        updated_at = pg_catalog.now()
    where case_id = new.id and status = 'active';
  end if;
  return new;
end;
$$;

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

drop trigger if exists agentic_rectification_cases_terminal_focus_guard
  on public.agentic_rectification_cases;
create trigger agentic_rectification_cases_terminal_focus_guard
after update of status on public.agentic_rectification_cases
for each row execute function public.agentic_rectification_terminal_focus_guard();

-- Existing cases receive a durable summary before the new dossier is used.
select public.refresh_agentic_rectification_case_conversation_summary(id)
from public.agentic_rectification_cases;

-- ---------------------------------------------------------------------------
-- 5. Focus RPCs. The server, not the model, validates the active target.
-- ---------------------------------------------------------------------------

create or replace function public.set_agentic_rectification_conversation_focus(
  p_user_id uuid,
  p_case_id uuid,
  p_question_id text,
  p_intent text,
  p_target_evidence_id uuid,
  p_target_domain text,
  p_target_kind text,
  p_expected_answer_schema jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_focus public.agentic_rectification_conversation_focuses%rowtype;
begin
  if p_user_id is null or p_case_id is null
    or length(btrim(coalesce(p_question_id, ''))) = 0
    or length(btrim(coalesce(p_intent, ''))) = 0
    or p_expected_answer_schema is null
    or jsonb_typeof(p_expected_answer_schema) <> 'object' then
    raise exception 'agentic_rectification_invalid_input' 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 v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;
  if p_target_evidence_id is not null and not exists (
    select 1 from public.agentic_rectification_evidence
    where id = p_target_evidence_id and case_id = p_case_id
  ) then
    raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
  end if;

  select * into v_focus
  from public.agentic_rectification_conversation_focuses
  where case_id = p_case_id and question_id = p_question_id;
  if found then
    if v_focus.status = 'active'
      and v_focus.intent = p_intent
      and v_focus.target_evidence_id is not distinct from p_target_evidence_id
      and v_focus.target_domain is not distinct from p_target_domain
      and v_focus.target_kind is not distinct from p_target_kind
      and v_focus.expected_answer_schema = p_expected_answer_schema then
      return jsonb_build_object(
        'focus_id', v_focus.id, 'status', v_focus.status, 'idempotent', true
      );
    end if;
    raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
  end if;

  update public.agentic_rectification_conversation_focuses
  set status = 'superseded',
      resolved_at = pg_catalog.now(),
      updated_at = pg_catalog.now()
  where case_id = p_case_id and status = 'active';

  insert into public.agentic_rectification_conversation_focuses (
    case_id, question_id, intent, target_evidence_id, target_domain, target_kind,
    expected_answer_schema, status, asked_at
  ) values (
    p_case_id, p_question_id, p_intent, p_target_evidence_id, p_target_domain, p_target_kind,
    p_expected_answer_schema, 'active', pg_catalog.now()
  ) returning * into v_focus;

  return jsonb_build_object(
    'focus_id', v_focus.id,
    'status', v_focus.status,
    'idempotent', false
  );
end;
$$;

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

create or replace function public.resolve_agentic_rectification_conversation_focus(
  p_user_id uuid,
  p_case_id uuid,
  p_focus_id uuid,
  p_status text,
  p_evidence_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_focus public.agentic_rectification_conversation_focuses%rowtype;
begin
  if p_user_id is null or p_case_id is null or p_focus_id is null
    or p_status not in ('resolved', 'declined', 'skipped', 'superseded') then
    raise exception 'agentic_rectification_invalid_input' 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;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;
  if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;

  select * into v_focus
  from public.agentic_rectification_conversation_focuses
  where id = p_focus_id and case_id = p_case_id
  for update;
  if not found then
    raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
  end if;
  if p_evidence_id is not null and not exists (
    select 1 from public.agentic_rectification_evidence
    where id = p_evidence_id and case_id = p_case_id
  ) then
    raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
  end if;
  if v_focus.target_evidence_id is not null
    and p_evidence_id is distinct from v_focus.target_evidence_id then
    raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
  end if;
  if v_focus.status <> 'active' then
    if v_focus.status = p_status
      and (v_focus.target_evidence_id is null or v_focus.target_evidence_id is not distinct from p_evidence_id) then
      return jsonb_build_object(
        'focus_id', v_focus.id, 'status', v_focus.status,
        'evidence_id', v_focus.target_evidence_id, 'idempotent', true
      );
    end if;
    raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
  end if;

  update public.agentic_rectification_conversation_focuses
  set status = p_status,
      target_evidence_id = coalesce(target_evidence_id, p_evidence_id),
      resolved_at = pg_catalog.now(),
      updated_at = pg_catalog.now()
  where id = v_focus.id;

  return jsonb_build_object(
    'focus_id', v_focus.id,
    'status', p_status,
    'evidence_id', coalesce(v_focus.target_evidence_id, p_evidence_id),
    'idempotent', false
  );
end;
$$;

revoke all on function public.resolve_agentic_rectification_conversation_focus(
  uuid, uuid, uuid, text, uuid
) from public, anon, authenticated;
grant execute on function public.resolve_agentic_rectification_conversation_focus(
  uuid, uuid, uuid, text, uuid
) to service_role;

create or replace function public.confirm_agentic_rectification_evidence_v10(
  p_user_id uuid,
  p_case_id uuid,
  p_focus_id uuid,
  p_evidence_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_focus public.agentic_rectification_conversation_focuses%rowtype;
  v_result jsonb;
begin
  if p_user_id is null or p_case_id is null or p_focus_id is null or p_evidence_id is null then
    raise exception 'agentic_rectification_invalid_input' 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 v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;
  if not exists (
    select 1 from public.agentic_rectification_evidence
    where id = p_evidence_id and case_id = p_case_id
  ) then
    raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
  end if;

  select * into v_focus
  from public.agentic_rectification_conversation_focuses
  where id = p_focus_id and case_id = p_case_id
  for update;
  if not found then
    raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
  end if;
  if v_focus.target_evidence_id is not null
    and v_focus.target_evidence_id <> p_evidence_id then
    raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
  end if;
  if v_focus.status not in ('active', 'resolved') then
    raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
  end if;
  if v_focus.status = 'resolved'
    and v_focus.target_evidence_id is distinct from p_evidence_id then
    raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
  end if;

  v_result := public.confirm_agentic_rectification_evidence(
    p_user_id, p_case_id, p_evidence_id
  );
  if v_focus.status = 'active' then
    perform public.resolve_agentic_rectification_conversation_focus(
      p_user_id, p_case_id, p_focus_id, 'resolved', p_evidence_id
    );
  end if;
  return v_result || jsonb_build_object('focus_id', p_focus_id);
end;
$$;

revoke all on function public.confirm_agentic_rectification_evidence_v10(uuid, uuid, uuid, uuid)
  from public, anon, authenticated;
grant execute on function public.confirm_agentic_rectification_evidence_v10(uuid, uuid, uuid, uuid)
  to service_role;

create or replace function public.revise_agentic_rectification_evidence_v10(
  p_user_id uuid,
  p_case_id uuid,
  p_focus_id uuid,
  p_evidence_id uuid,
  p_user_quote text,
  p_occurred_from date,
  p_occurred_to date,
  p_date_precision text,
  p_summary text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_focus public.agentic_rectification_conversation_focuses%rowtype;
  v_existing public.agentic_rectification_evidence%rowtype;
  v_result jsonb;
begin
  if p_user_id is null or p_case_id is null or p_focus_id is null or p_evidence_id is null then
    raise exception 'agentic_rectification_invalid_input' 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 v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;

  select * into v_focus
  from public.agentic_rectification_conversation_focuses
  where id = p_focus_id and case_id = p_case_id
  for update;
  if not found then
    raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
  end if;
  if v_focus.target_evidence_id is not null
    and v_focus.target_evidence_id <> p_evidence_id then
    raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
  end if;
  if v_focus.status not in ('active', 'resolved') then
    raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
  end if;

  if v_focus.status = 'resolved' then
    if v_focus.target_evidence_id is distinct from p_evidence_id then
      raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
    end if;
    select * into v_existing
    from public.agentic_rectification_evidence
    where case_id = p_case_id
      and supersedes_evidence_id = p_evidence_id
      and user_quote = p_user_quote
      and occurred_from is not distinct from p_occurred_from
      and occurred_to is not distinct from p_occurred_to
      and date_precision = p_date_precision
      and summary = p_summary
    order by created_at desc, id desc
    limit 1;
    if not found then
      raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
    end if;
    return jsonb_build_object(
      'evidence_id', v_existing.id,
      'supersedes_evidence_id', p_evidence_id,
      'focus_id', p_focus_id,
      'idempotent', true
    );
  end if;

  v_result := public.revise_agentic_rectification_evidence(
    p_user_id, p_case_id, p_evidence_id, p_user_quote,
    p_occurred_from, p_occurred_to, p_date_precision, p_summary
  );
  perform public.resolve_agentic_rectification_conversation_focus(
    p_user_id, p_case_id, p_focus_id, 'resolved', p_evidence_id
  );
  return v_result || jsonb_build_object('focus_id', p_focus_id);
end;
$$;

revoke all on function public.revise_agentic_rectification_evidence_v10(
  uuid, uuid, uuid, uuid, text, date, date, text, text
) from public, anon, authenticated;
grant execute on function public.revise_agentic_rectification_evidence_v10(
  uuid, uuid, uuid, uuid, text, date, date, text, text
) to service_role;

-- ---------------------------------------------------------------------------
-- 6. Batch evidence: per-item validation/result and transactional idempotency.
-- ---------------------------------------------------------------------------

create or replace function public.record_agentic_rectification_evidence_batch(
  p_user_id uuid,
  p_case_id uuid,
  p_source_turn_id uuid,
  p_focus_id uuid,
  p_items jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_turn public.agentic_rectification_turns%rowtype;
  v_focus public.agentic_rectification_conversation_focuses%rowtype;
  v_focus_replay boolean := false;
  v_focus_match_id uuid;
  v_focus_match_count integer := 0;
  v_focus_resolution text := 'not_requested';
  v_item jsonb;
  v_index bigint;
  v_item_key text;
  v_quote text;
  v_subject text;
  v_kind text;
  v_domain text;
  v_precision text;
  v_summary text;
  v_from date;
  v_to date;
  v_status text;
  v_outcome text;
  v_error text;
  v_clarification jsonb;
  v_existing public.agentic_rectification_evidence%rowtype;
  v_evidence_id uuid;
  v_idempotent boolean;
  v_results jsonb := '[]'::jsonb;
  v_accepted integer := 0;
  v_needs integer := 0;
  v_rejected integer := 0;
begin
  if p_user_id is null or p_case_id is null or p_source_turn_id is null
    or p_items is null or jsonb_typeof(p_items) <> 'array'
    or jsonb_array_length(p_items) = 0 or jsonb_array_length(p_items) > 12 then
    raise exception 'agentic_rectification_invalid_input' 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 v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;

  select * into v_turn
  from public.agentic_rectification_turns
  where id = p_source_turn_id and case_id = p_case_id;
  if not found then
    raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
  end if;
  if v_turn.user_message is null then
    raise exception 'agentic_rectification_quote_not_grounded' using errcode = 'P0001';
  end if;

  if p_focus_id is not null then
    select * into v_focus
    from public.agentic_rectification_conversation_focuses
    where id = p_focus_id and case_id = p_case_id
    for update;
    if not found or v_focus.status not in ('active', 'resolved') then
      raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
    end if;
    v_focus_replay := v_focus.status = 'resolved';
    if not v_focus_replay and v_focus.target_evidence_id is not null then
      raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
    end if;
    v_focus_resolution := case when v_focus_replay then 'idempotent' else 'active' end;
    if v_focus_replay and exists (
      select 1
      from jsonb_array_elements(p_items) item
      where length(btrim(coalesce(item->>'idempotency_key', ''))) = 0
        or not exists (
          select 1 from public.agentic_rectification_evidence e
          where e.case_id = p_case_id
            and e.idempotency_key = btrim(item->>'idempotency_key')
        )
    ) then
      raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
    end if;
  end if;

  for v_item, v_index in
    select value, ordinality
    from jsonb_array_elements(p_items) with ordinality
  loop
    v_item_key := btrim(coalesce(v_item->>'idempotency_key', ''));
    v_quote := btrim(coalesce(v_item->>'quote', ''));
    v_subject := coalesce(v_item->>'subject', '');
    v_kind := coalesce(v_item->>'event_kind', '');
    v_domain := coalesce(v_item->>'domain', '');
    v_precision := coalesce(v_item->>'date_precision', '');
    v_summary := btrim(coalesce(v_item->>'summary', ''));
    v_from := null;
    v_to := null;
    v_error := null;
    v_clarification := '[]'::jsonb;
    v_evidence_id := null;
    v_idempotent := false;

    begin
      if nullif(v_item->>'occurred_from', '') is not null then
        v_from := (v_item->>'occurred_from')::date;
      end if;
      if nullif(v_item->>'occurred_to', '') is not null then
        v_to := (v_item->>'occurred_to')::date;
      end if;
    exception when others then
      v_error := 'invalid_date';
    end;

    if v_error is null and (
      length(v_item_key) = 0 or length(v_item_key) > 160
      or length(v_quote) = 0 or length(v_summary) = 0
      or v_subject not in ('self', 'family', 'other')
      or v_kind not in (
        'education_start', 'education_completion', 'education_interruption',
        'career_entry', 'career_change', 'promotion', 'career_pressure', 'career_exit',
        'relationship_start', 'relationship_commitment', 'relationship_separation',
        'relocation', 'finance_gain', 'finance_loss',
        'self_health_event', 'family_event', 'other'
      )
      or v_domain not in ('education', 'career', 'relationship', 'relocation', 'finance', 'health', 'family', 'other')
      or v_precision not in ('year', 'month', 'day', 'range', 'unknown')
    ) then
      v_error := 'invalid_item';
    end if;

    if v_error is null and position(
      public.agentic_rectification_normalize_quote(v_quote)
      in public.agentic_rectification_normalize_quote(v_turn.user_message)
    ) = 0 then
      v_error := 'quote_not_grounded';
    end if;

    if v_error is null and v_precision = 'range'
      and (v_from is null or v_to is null or v_from > v_to) then
      v_error := 'invalid_range';
    end if;

    if v_error is not null then
      v_outcome := 'rejected';
      v_status := 'rejected';
      v_rejected := v_rejected + 1;
    else
      select * into v_existing
      from public.agentic_rectification_evidence
      where case_id = p_case_id and idempotency_key = v_item_key;

      if found then
        if v_existing.source_turn_id is distinct from p_source_turn_id
          or v_existing.user_quote is distinct from v_quote
          or v_existing.subject is distinct from v_subject
          or v_existing.event_kind is distinct from v_kind
          or v_existing.domain is distinct from v_domain
          or v_existing.occurred_from is distinct from v_from
          or v_existing.occurred_to is distinct from v_to
          or v_existing.date_precision is distinct from v_precision
          or v_existing.summary is distinct from v_summary then
          v_outcome := 'rejected';
          v_status := 'rejected';
          v_error := 'idempotency_conflict';
          v_rejected := v_rejected + 1;
        else
          v_evidence_id := v_existing.id;
          v_idempotent := true;
          if v_existing.status = 'confirmed' then
            v_outcome := 'accepted';
            v_status := 'confirmed';
            v_accepted := v_accepted + 1;
          else
            v_outcome := 'needs_clarification';
            v_status := v_existing.status;
            v_clarification := case
              when v_existing.date_precision = 'unknown' then '["date"]'::jsonb
              else '[]'::jsonb
            end;
            v_needs := v_needs + 1;
          end if;
        end if;
      else
        if v_precision = 'unknown' or v_from is null then
          v_outcome := 'needs_clarification';
          v_status := 'draft';
          v_clarification := '["date"]'::jsonb;
          v_needs := v_needs + 1;
        else
          v_outcome := 'accepted';
          v_status := 'confirmed';
          v_accepted := v_accepted + 1;
        end if;

        insert into public.agentic_rectification_evidence (
          case_id, source_turn_id, user_quote, subject, event_kind, domain,
          occurred_from, occurred_to, date_precision, summary, status,
          confirmed_at, idempotency_key
        ) values (
          p_case_id, p_source_turn_id, v_quote, v_subject, v_kind, v_domain,
          v_from, v_to, v_precision, v_summary, v_status,
          case when v_status = 'confirmed' then pg_catalog.now() else null end,
          v_item_key
        ) returning id into v_evidence_id;
      end if;
    end if;

    if p_focus_id is not null
      and v_outcome = 'accepted'
      and v_evidence_id is not null
      and (v_focus.target_domain is null or v_domain = v_focus.target_domain)
      and (v_focus.target_kind is null or v_kind = v_focus.target_kind) then
      v_focus_match_count := v_focus_match_count + 1;
      if v_focus_match_id is null then
        v_focus_match_id := v_evidence_id;
      end if;
    end if;

    v_results := v_results || jsonb_build_array(jsonb_build_object(
      'index', v_index - 1,
      'idempotency_key', nullif(v_item_key, ''),
      'outcome', v_outcome,
      'evidence_id', v_evidence_id,
      'status', v_status,
      'idempotent', v_idempotent,
      'clarification_fields', v_clarification,
      'error_code', v_error
    ));
  end loop;

  update public.agentic_rectification_cases
  set status = case when status = 'draft' and v_accepted > 0 then 'collecting_evidence' else status end,
      last_activity_at = pg_catalog.now(),
      updated_at = pg_catalog.now()
  where id = p_case_id;

  if p_focus_id is not null then
    if v_focus_replay then
      if v_focus_match_count <> 1
        or v_focus_match_id is distinct from v_focus.target_evidence_id then
        raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
      end if;
      v_focus_resolution := 'idempotent';
    elsif v_focus_match_count = 1 then
      perform public.resolve_agentic_rectification_conversation_focus(
        p_user_id, p_case_id, p_focus_id, 'resolved', v_focus_match_id
      );
      v_focus_resolution := 'resolved';
    elsif v_focus_match_count > 1 then
      -- Do not guess which evidence answered the question. Keep the focus active
      -- and make the ambiguity explicit to the caller.
      v_focus_resolution := 'ambiguous';
    else
      v_focus_resolution := 'unmatched';
    end if;
  end if;

  return jsonb_build_object(
    'items', v_results,
    'accepted_count', v_accepted,
    'needs_clarification_count', v_needs,
    'rejected_count', v_rejected,
    'focus_id', p_focus_id,
    'focus_evidence_id', v_focus_match_id,
    'focus_resolution', v_focus_resolution
  );
end;
$$;

revoke all on function public.record_agentic_rectification_evidence_batch(
  uuid, uuid, uuid, uuid, jsonb
) from public, anon, authenticated;
grant execute on function public.record_agentic_rectification_evidence_batch(
  uuid, uuid, uuid, uuid, jsonb
) to service_role;

-- ---------------------------------------------------------------------------
-- 7. Attempt lifecycle and attempt-aware receipt insertions.
-- ---------------------------------------------------------------------------

-- Request-aware turn append. The seven-argument V9 function remains available.
create or replace function public.append_agentic_rectification_turn(
  p_user_id uuid,
  p_case_id uuid,
  p_user_message text,
  p_assistant_message text,
  p_model_name text,
  p_model_version text,
  p_status text,
  p_request_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_turn public.agentic_rectification_turns%rowtype;
begin
  if p_user_id is null or p_case_id is null or p_request_id is null
    or length(btrim(coalesce(p_model_name, ''))) = 0
    or p_status not in ('pending', 'completed', 'failed', 'retryable') then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;
  if p_status = 'completed'
    and (p_assistant_message is null or length(btrim(p_assistant_message)) = 0) then
    raise exception 'agentic_rectification_turn_incomplete' 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;

  select * into v_turn
  from public.agentic_rectification_turns
  where case_id = p_case_id and request_id = p_request_id
  for update;
  if found then
    if v_turn.user_message is distinct from p_user_message
      or v_turn.model_name is distinct from p_model_name
      or v_turn.model_version is distinct from p_model_version then
      raise exception 'agentic_rectification_request_mismatch' using errcode = 'P0001';
    end if;
    return jsonb_build_object(
      'turn_id', v_turn.id,
      'status', v_turn.status,
      'assistant_message', v_turn.assistant_message,
      'successful_attempt_id', v_turn.successful_attempt_id,
      'should_execute', false,
      'already_in_progress', v_turn.status = 'pending',
      'idempotent', true
    );
  end if;

  if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;

  insert into public.agentic_rectification_turns (
    case_id, request_id, user_message, assistant_message, status,
    model_name, model_version, completed_at
  ) values (
    p_case_id, p_request_id, p_user_message, p_assistant_message, p_status,
    p_model_name, p_model_version,
    case when p_status = 'completed' then pg_catalog.now() else null end
  ) returning * into v_turn;

  update public.agentic_rectification_cases
  set last_activity_at = pg_catalog.now(),
      updated_at = pg_catalog.now()
  where id = p_case_id;

  return jsonb_build_object(
    'turn_id', v_turn.id,
    'status', v_turn.status,
    'assistant_message', v_turn.assistant_message,
    'successful_attempt_id', v_turn.successful_attempt_id,
    'should_execute', true,
    'already_in_progress', false,
    'idempotent', false
  );
end;
$$;

revoke all on function public.append_agentic_rectification_turn(
  uuid, uuid, text, text, text, text, text, uuid
) from public, anon, authenticated;
grant execute on function public.append_agentic_rectification_turn(
  uuid, uuid, text, text, text, text, text, uuid
) to service_role;

-- V10 request idempotency is mandatory for every runtime Turn write. Keep the
-- V9 overload for migration compatibility, but remove it from service runtime.
revoke execute on function public.append_agentic_rectification_turn(
  uuid, uuid, text, text, text, text, text
) from service_role;

create or replace function public.create_agentic_rectification_run_attempt(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid,
  p_attempt_number integer
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_turn public.agentic_rectification_turns%rowtype;
  v_attempt public.agentic_rectification_run_attempts%rowtype;
  v_inserted boolean := false;
begin
  if p_user_id is null or p_case_id is null or p_turn_id is null
    or coalesce(p_attempt_number, 0) <= 0 then
    raise exception 'agentic_rectification_invalid_input' 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;
  select * into v_turn
  from public.agentic_rectification_turns
  where id = p_turn_id and case_id = p_case_id
  for update;
  if not found then
    raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
  end if;

  -- A terminal Case must not create a new attempt, but a worker recovering
  -- after a confirm/close action still needs the identity of the attempt that
  -- already owned execution so it can finish the durable Turn projection.
  select * into v_attempt
  from public.agentic_rectification_run_attempts
  where turn_id = p_turn_id and attempt_number = p_attempt_number
  for update;
  if found then
    return jsonb_build_object(
      'attempt_id', v_attempt.id,
      'attempt_number', v_attempt.attempt_number,
      'status', v_attempt.status,
      'should_execute', false,
      'already_in_progress', v_attempt.status = 'started',
      'idempotent', true
    );
  end if;

  if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;

  insert into public.agentic_rectification_run_attempts (
    case_id, turn_id, attempt_number, status
  ) values (
    p_case_id, p_turn_id, p_attempt_number, 'started'
  )
  on conflict (turn_id, attempt_number) do nothing
  returning * into v_attempt;
  v_inserted := found;

  if not v_inserted then
    select * into v_attempt
    from public.agentic_rectification_run_attempts
    where turn_id = p_turn_id and attempt_number = p_attempt_number
    for update;
  end if;

  return jsonb_build_object(
    'attempt_id', v_attempt.id,
    'attempt_number', v_attempt.attempt_number,
    'status', v_attempt.status,
    'should_execute', v_inserted,
    'already_in_progress', not v_inserted and v_attempt.status = 'started',
    'idempotent', not v_inserted
  );
end;
$$;

revoke all on function public.create_agentic_rectification_run_attempt(uuid, uuid, uuid, integer)
  from public, anon, authenticated;
grant execute on function public.create_agentic_rectification_run_attempt(uuid, uuid, uuid, integer)
  to service_role;

create or replace function public.finalize_agentic_rectification_run_attempt(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid,
  p_attempt_id uuid,
  p_status text,
  p_error_code text,
  p_usage jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_attempt public.agentic_rectification_run_attempts%rowtype;
  v_error_code text := nullif(btrim(coalesce(p_error_code, '')), '');
begin
  if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
    or p_status not in ('completed', 'failed', 'retryable', 'aborted')
    or p_usage is null or jsonb_typeof(p_usage) <> 'object' then
    raise exception 'agentic_rectification_invalid_input' 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;
  select * into v_attempt
  from public.agentic_rectification_run_attempts
  where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
  for update;
  if not found then
    raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
  end if;
  if v_attempt.status <> 'started' then
    if v_attempt.status = p_status
      and v_attempt.error_code is not distinct from v_error_code
      and v_attempt.usage = p_usage then
      return jsonb_build_object(
        'attempt_id', v_attempt.id, 'status', v_attempt.status, 'idempotent', true
      );
    end if;
    raise exception 'agentic_rectification_attempt_already_finalized' using errcode = 'P0001';
  end if;

  if p_status = 'completed' then
    if not exists (
      select 1 from public.agentic_rectification_run_phases
      where attempt_id = p_attempt_id
        and case_id = p_case_id
        and turn_id = p_turn_id
        and phase = 'billing.settled'
    ) or not exists (
      select 1 from public.agentic_rectification_run_phases
      where attempt_id = p_attempt_id
        and case_id = p_case_id
        and turn_id = p_turn_id
        and phase = 'run.completed'
    ) then
      raise exception 'agentic_rectification_attempt_completion_receipt_missing' using errcode = 'P0001';
    end if;

    if not exists (
      select 1
      from public.agentic_rectification_skill_run_receipts receipt
      where receipt.case_id = p_case_id
        and receipt.turn_id = p_turn_id
        and receipt.request_id = p_attempt_id
        and receipt.run_kind = 'turn'
        and receipt.user_id = p_user_id
        and receipt.skill_name = v_case.skill_name
        and receipt.skill_version = v_case.skill_version
        and receipt.skill_sha256 = v_case.skill_sha256
        and receipt.source_commit is not distinct from v_case.skill_source_commit
    ) then
      raise exception 'agentic_rectification_skill_receipt_missing' using errcode = 'P0001';
    end if;
  end if;

  update public.agentic_rectification_run_attempts
  set status = p_status,
      error_code = v_error_code,
      usage = p_usage,
      completed_at = pg_catalog.now(),
      updated_at = pg_catalog.now()
  where id = p_attempt_id;

  return jsonb_build_object(
    'attempt_id', p_attempt_id, 'status', p_status, 'idempotent', false
  );
end;
$$;

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

-- New overload. The six-argument V9 function remains untouched.
create or replace function public.insert_agentic_rectification_run_phase(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid,
  p_phase text,
  p_tool_name text,
  p_sequence integer,
  p_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_attempt public.agentic_rectification_run_attempts%rowtype;
  v_phase_id uuid;
begin
  if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
    or p_phase not in (
      'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
      'intent.classified', 'evidence.proposed', 'evidence.confirmed',
      'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
      'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
      'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
    ) then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;
  if not exists (
    select 1 from public.agentic_rectification_cases
    where id = p_case_id and user_id = p_user_id
  ) 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;

  select * into v_attempt
  from public.agentic_rectification_run_attempts
  where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
  for update;
  if not found then
    raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
  end if;
  if v_attempt.status <> 'started' then
    raise exception 'agentic_rectification_attempt_not_started' using errcode = 'P0001';
  end if;

  insert into public.agentic_rectification_run_phases (
    case_id, turn_id, attempt_id, phase, tool_name, sequence
  ) values (
    p_case_id, p_turn_id, p_attempt_id, p_phase, p_tool_name, coalesce(p_sequence, 0)
  ) returning id into v_phase_id;

  return jsonb_build_object('phase_id', v_phase_id, 'attempt_id', p_attempt_id);
end;
$$;

revoke all on function public.insert_agentic_rectification_run_phase(
  uuid, uuid, uuid, text, text, integer, uuid
) from public, anon, authenticated;
grant execute on function public.insert_agentic_rectification_run_phase(
  uuid, uuid, uuid, text, text, integer, uuid
) to service_role;

-- New overload. The eleven-argument V9 function remains untouched.
create or replace function public.insert_agentic_rectification_tool_receipt(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid,
  p_tool_name text,
  p_public_phase text,
  p_status text,
  p_input_fingerprint text,
  p_result_fingerprint text,
  p_engine_version text,
  p_safe_error_code text,
  p_executed_methods jsonb,
  p_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_attempt public.agentic_rectification_run_attempts%rowtype;
  v_receipt_id uuid;
begin
  if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
    or p_tool_name not in (
      'rectification-read-case', 'rectification-set-focus',
      'rectification-resolve-focus', 'rectification-record-evidence-batch',
      'rectification-propose-evidence', 'rectification-confirm-evidence',
      'rectification-revise-evidence', 'rectification-compare-candidates',
      'rectification-read-diagnostics', 'rectification-offer-candidates',
      'rectification-accept-candidate', 'rectification-confirm-birth-time',
      'rectification-close-case'
    )
    or p_public_phase not in (
      'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
      'intent.classified', 'evidence.proposed', 'evidence.confirmed',
      'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
      'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
      'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
    )
    or p_status not in ('started', 'completed', 'failed', 'skipped')
    or p_executed_methods is null or jsonb_typeof(p_executed_methods) <> 'array'
    or exists (
      select 1 from jsonb_array_elements_text(p_executed_methods) method(value)
      where method.value not in (
        'd1-rashi', 'd2-hora', 'd4-chaturthamsha', 'd9-navamsa',
        'd10-dashamsa', 'd11-labhamsha', 'd24-chaturvimshamsha',
        'd30-trimshamsha', 'vimshottari-dasha', 'narayana-dasha',
        'gochara', 'ashtakavarga', 'shadbala', 'arudha-pada',
        'functional-benefic-malefic'
      )
    ) then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;
  if not exists (
    select 1 from public.agentic_rectification_cases
    where id = p_case_id and user_id = p_user_id
  ) 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;

  select * into v_attempt
  from public.agentic_rectification_run_attempts
  where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
  for update;
  if not found then
    raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
  end if;
  if v_attempt.status <> 'started' then
    raise exception 'agentic_rectification_attempt_not_started' using errcode = 'P0001';
  end if;

  insert into public.agentic_rectification_tool_receipts (
    case_id, turn_id, attempt_id, tool_name, public_phase, status,
    input_fingerprint, result_fingerprint, engine_version, safe_error_code,
    executed_methods, completed_at
  ) values (
    p_case_id, p_turn_id, p_attempt_id, p_tool_name, p_public_phase, p_status,
    p_input_fingerprint, p_result_fingerprint, p_engine_version, p_safe_error_code,
    p_executed_methods,
    case when p_status = 'completed' then pg_catalog.now() else null end
  ) returning id into v_receipt_id;

  return jsonb_build_object('receipt_id', v_receipt_id, 'attempt_id', p_attempt_id);
end;
$$;

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

-- Attempt-aware finalization. The five-argument V9 function remains available.
create or replace function public.finalize_agentic_rectification_turn(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid,
  p_status text,
  p_assistant_message text,
  p_successful_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_turn public.agentic_rectification_turns%rowtype;
  v_attempt public.agentic_rectification_run_attempts%rowtype;
begin
  if p_user_id is null or p_case_id is null or p_turn_id is null
    or p_status not in ('completed', 'failed', 'retryable') then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;
  if not exists (
    select 1 from public.agentic_rectification_cases
    where id = p_case_id and user_id = p_user_id
  ) then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;

  select * into v_turn
  from public.agentic_rectification_turns
  where id = p_turn_id and case_id = p_case_id
  for update;
  if not found then
    raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
  end if;

  if v_turn.status = 'completed' then
    if p_status = 'completed'
      and v_turn.successful_attempt_id is not distinct from p_successful_attempt_id
      and v_turn.assistant_message is not distinct from p_assistant_message then
      return jsonb_build_object(
        'turn_id', p_turn_id,
        'status', v_turn.status,
        'successful_attempt_id', v_turn.successful_attempt_id,
        'idempotent', true
      );
    end if;
    raise exception 'agentic_rectification_turn_already_completed' using errcode = 'P0001';
  end if;

  if p_status = 'completed' then
    if p_successful_attempt_id is null
      or p_assistant_message is null
      or length(btrim(p_assistant_message)) = 0 then
      raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
    end if;
    select * into v_attempt
    from public.agentic_rectification_run_attempts
    where id = p_successful_attempt_id
      and case_id = p_case_id
      and turn_id = p_turn_id
      and status = 'completed';
    if not found then
      raise exception 'agentic_rectification_attempt_not_successful' using errcode = 'P0001';
    end if;
  elsif p_successful_attempt_id is not null or p_assistant_message is not null then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;

  update public.agentic_rectification_turns
  set status = p_status,
      assistant_message = case when p_status = 'completed' then p_assistant_message else null end,
      successful_attempt_id = case when p_status = 'completed' then p_successful_attempt_id else null end,
      completed_at = case when p_status = 'completed' then pg_catalog.now() else null end,
      updated_at = pg_catalog.now()
  where id = p_turn_id and status <> 'completed';

  return jsonb_build_object(
    'turn_id', p_turn_id,
    'status', p_status,
    'successful_attempt_id', case when p_status = 'completed' then p_successful_attempt_id else null end,
    'idempotent', false
  );
end;
$$;

revoke all on function public.finalize_agentic_rectification_turn(
  uuid, uuid, uuid, text, text, uuid
) from public, anon, authenticated;

-- Legacy finalizers cannot prove latest-attempt ownership. Retain their
-- definitions for additive migration compatibility, but make the V10 runtime
-- use only the attempt-aware overload below.
revoke execute on function public.finalize_agentic_rectification_turn(
  uuid, uuid, uuid, text, text
) from service_role;
revoke execute on function public.finalize_agentic_rectification_turn(
  uuid, uuid, uuid, text, text, uuid
) from service_role;

-- Attempt-aware monotonic finalization used by the V10 runner.
create or replace function public.finalize_agentic_rectification_turn(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid,
  p_attempt_id uuid,
  p_status text,
  p_assistant_message text,
  p_successful_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_turn public.agentic_rectification_turns%rowtype;
  v_attempt public.agentic_rectification_run_attempts%rowtype;
  v_latest_attempt_id uuid;
begin
  if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
    or p_status not in ('completed', 'failed', 'retryable') then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;
  if not exists (
    select 1 from public.agentic_rectification_cases
    where id = p_case_id and user_id = p_user_id
  ) then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;

  select * into v_turn
  from public.agentic_rectification_turns
  where id = p_turn_id and case_id = p_case_id
  for update;
  if not found then
    raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
  end if;

  if v_turn.status = 'completed' then
    if p_status = 'completed'
      and p_attempt_id = p_successful_attempt_id
      and v_turn.successful_attempt_id = p_successful_attempt_id
      and v_turn.assistant_message is not distinct from p_assistant_message then
      return jsonb_build_object(
        'turn_id', p_turn_id,
        'status', v_turn.status,
        'successful_attempt_id', v_turn.successful_attempt_id,
        'idempotent', true
      );
    end if;
    raise exception 'agentic_rectification_turn_already_completed' using errcode = 'P0001';
  end if;

  select * into v_attempt
  from public.agentic_rectification_run_attempts
  where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
  for update;
  if not found then
    raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
  end if;

  select id into v_latest_attempt_id
  from public.agentic_rectification_run_attempts
  where case_id = p_case_id and turn_id = p_turn_id
  order by attempt_number desc
  limit 1;
  if v_latest_attempt_id is distinct from p_attempt_id then
    raise exception 'agentic_rectification_attempt_superseded' using errcode = 'P0001';
  end if;

  if p_status = 'completed' then
    if p_successful_attempt_id is distinct from p_attempt_id
      or p_assistant_message is null
      or length(btrim(p_assistant_message)) = 0
      or v_attempt.status <> 'completed' then
      raise exception 'agentic_rectification_attempt_not_successful' using errcode = 'P0001';
    end if;
  else
    if p_successful_attempt_id is not null or p_assistant_message is not null
      or v_attempt.status <> p_status then
      raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
    end if;
    if v_turn.status = p_status then
      return jsonb_build_object(
        'turn_id', p_turn_id,
        'status', v_turn.status,
        'successful_attempt_id', null,
        'idempotent', true
      );
    end if;
  end if;

  update public.agentic_rectification_turns
  set status = p_status,
      assistant_message = case when p_status = 'completed' then p_assistant_message else null end,
      successful_attempt_id = case when p_status = 'completed' then p_successful_attempt_id else null end,
      completed_at = case when p_status = 'completed' then pg_catalog.now() else null end,
      updated_at = pg_catalog.now()
  where id = p_turn_id and status <> 'completed';

  return jsonb_build_object(
    'turn_id', p_turn_id,
    'status', p_status,
    'successful_attempt_id', case when p_status = 'completed' then p_successful_attempt_id else null end,
    'idempotent', false
  );
end;
$$;

revoke all on function public.finalize_agentic_rectification_turn(
  uuid, uuid, uuid, uuid, text, text, uuid
) from public, anon, authenticated;
grant execute on function public.finalize_agentic_rectification_turn(
  uuid, uuid, uuid, uuid, text, text, uuid
) to service_role;

-- ---------------------------------------------------------------------------
-- 8. Attempt-filtered turn receipt and bounded dossier with durable summary.
-- ---------------------------------------------------------------------------

create or replace function public.get_agentic_rectification_turn_receipt(
  p_user_id uuid,
  p_case_id uuid,
  p_turn_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_turn public.agentic_rectification_turns%rowtype;
  v_case public.agentic_rectification_cases%rowtype;
  v_attempt_id uuid;
  v_phases jsonb;
  v_tools jsonb;
  v_methods jsonb;
  v_engine_version text;
begin
  if p_user_id is null or p_case_id is null or p_turn_id is null then
    raise exception 'agentic_rectification_invalid_input' 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;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;
  select * into v_turn
  from public.agentic_rectification_turns
  where id = p_turn_id and case_id = p_case_id;
  if not found then
    raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
  end if;

  v_attempt_id := v_turn.successful_attempt_id;
  if v_attempt_id is null then
    select id into v_attempt_id
    from public.agentic_rectification_run_attempts
    where turn_id = p_turn_id and status = 'completed'
    order by completed_at desc, attempt_number desc
    limit 1;
  end if;

  select coalesce(jsonb_agg(
    jsonb_build_object('phase', rp.phase, 'tool', rp.tool_name)
    order by rp.sequence, rp.created_at, rp.id
  ), '[]'::jsonb) into v_phases
  from public.agentic_rectification_run_phases rp
  where rp.turn_id = p_turn_id
    and (
      (v_attempt_id is not null and rp.attempt_id = v_attempt_id)
      or (v_attempt_id is null and rp.attempt_id is null)
    );

  select coalesce(jsonb_agg(tool_name order by tool_name), '[]'::jsonb) into v_tools
  from (
    select distinct tr.tool_name
    from public.agentic_rectification_tool_receipts tr
    where tr.turn_id = p_turn_id and tr.status = 'completed'
      and (
        (v_attempt_id is not null and tr.attempt_id = v_attempt_id)
        or (v_attempt_id is null and tr.attempt_id is null)
      )
  ) tools;

  select coalesce(jsonb_agg(method order by method), '[]'::jsonb) into v_methods
  from (
    select distinct jsonb_array_elements_text(tr.executed_methods) as method
    from public.agentic_rectification_tool_receipts tr
    where tr.turn_id = p_turn_id and tr.status = 'completed'
      and (
        (v_attempt_id is not null and tr.attempt_id = v_attempt_id)
        or (v_attempt_id is null and tr.attempt_id is null)
      )
  ) methods;

  select max(tr.engine_version) into v_engine_version
  from public.agentic_rectification_tool_receipts tr
  where tr.turn_id = p_turn_id and tr.engine_version is not null
    and (
      (v_attempt_id is not null and tr.attempt_id = v_attempt_id)
      or (v_attempt_id is null and tr.attempt_id is null)
    );

  return jsonb_build_object(
    'turn_id', v_turn.id,
    'attempt_id', v_attempt_id,
    'status', v_turn.status,
    'skill_name', v_case.skill_name,
    'skill_version', v_case.skill_version,
    'engine_version', v_engine_version,
    'phases', v_phases,
    'tools', v_tools,
    'methods', v_methods,
    'started_at', v_turn.created_at,
    'completed_at', v_turn.completed_at
  );
end;
$$;

revoke all on function public.get_agentic_rectification_turn_receipt(uuid, uuid, uuid)
  from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_turn_receipt(uuid, uuid, uuid)
  to service_role;

create or replace function public.get_agentic_rectification_case_dossier(
  p_user_id uuid,
  p_case_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_turns jsonb;
  v_evidence jsonb;
  v_summary public.agentic_rectification_case_conversation_summaries%rowtype;
  v_result public.agentic_rectification_results%rowtype;
  v_evidence_count bigint;
  v_turn_count bigint;
begin
  if p_user_id is null or p_case_id is null then
    raise exception 'agentic_rectification_invalid_input' 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;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;

  select coalesce(jsonb_agg(jsonb_build_object(
    'id', recent.id,
    'role', message.role,
    'text', message.text,
    'status', recent.status,
    'created_at', recent.created_at,
    'completed_at', recent.completed_at
  ) order by recent.created_at, recent.id, message.ordinal), '[]'::jsonb)
  into v_turns
  from (
    select t.*
    from public.agentic_rectification_turns t
    where t.case_id = v_case.id
    order by t.created_at desc, t.id desc
    limit 50
  ) recent
  cross join lateral (
    values
      (1, 'user'::text, recent.user_message),
      (2, 'assistant'::text, recent.assistant_message)
  ) as message(ordinal, role, text)
  where message.text is not null;

  select coalesce(jsonb_agg(jsonb_build_object(
    'id', e.id,
    'source_turn_id', e.source_turn_id,
    'subject', e.subject,
    'event_kind', e.event_kind,
    'domain', e.domain,
    'occurred_from', e.occurred_from,
    'occurred_to', e.occurred_to,
    'date_precision', e.date_precision,
    'summary', e.summary,
    'status', e.status,
    'supersedes_evidence_id', e.supersedes_evidence_id,
    'created_at', e.created_at
  ) order by e.created_at, e.id), '[]'::jsonb)
  into v_evidence
  from public.agentic_rectification_evidence e
  where e.case_id = v_case.id;

  select count(*) into v_evidence_count
  from public.agentic_rectification_evidence where case_id = v_case.id;
  select count(*) into v_turn_count
  from public.agentic_rectification_turns where case_id = v_case.id;

  select * into v_summary
  from public.agentic_rectification_case_conversation_summaries
  where case_id = v_case.id;
  if not found then
    perform public.refresh_agentic_rectification_case_conversation_summary(v_case.id);
    select * into v_summary
    from public.agentic_rectification_case_conversation_summaries
    where case_id = v_case.id;
  end if;

  select * into v_result
  from public.agentic_rectification_results
  where case_id = v_case.id and invalidated_at is null
  order by created_at desc, id desc
  limit 1;

  return jsonb_build_object(
    'case', jsonb_build_object(
      'case_id', v_case.id,
      'session_id', v_case.session_id,
      'status', v_case.status,
      'skill_name', v_case.skill_name,
      'skill_version', v_case.skill_version,
      'candidate_range', v_case.candidate_range,
      'accepted_time', v_case.accepted_time,
      'confirmed_time', v_case.confirmed_time,
      'completed_at', v_case.completed_at,
      'closed_reason', v_case.closed_reason,
      'last_activity_at', v_case.last_activity_at,
      'evidence_count', v_evidence_count,
      'turn_count', v_turn_count
    ),
    'turns', v_turns,
    'evidence', v_evidence,
    'conversation_summary', jsonb_build_object(
      'confirmed_evidence_summary', v_summary.confirmed_evidence_summary,
      'pending_revisions', v_summary.pending_revisions,
      'active_focus', v_summary.active_focus,
      'declined_skipped_topics', v_summary.declined_skipped_topics,
      'candidate_divergence_summary', v_summary.candidate_divergence_summary,
      'missing_evidence_categories', v_summary.missing_evidence_categories,
      'last_result_policy', v_summary.last_result_policy,
      'summary_version', v_summary.summary_version,
      'updated_at', v_summary.updated_at
    ),
    'latest_result', case when v_result.id is null then null else jsonb_build_object(
      'result_id', v_result.id,
      'candidates', v_result.candidates,
      'overall_confidence', v_result.overall_confidence,
      'selection_allowed', v_result.selection_allowed,
      'confirmation_allowed', v_result.confirmation_allowed,
      'representative_time', v_result.representative_time,
      'selected_time', v_result.selected_time,
      'selection_kind', v_result.selection_kind,
      'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint,
      'candidate_range_fingerprint', v_result.candidate_range_fingerprint,
      'skill_version', v_result.skill_version,
      'algorithm_version', v_result.algorithm_version,
      'created_at', v_result.created_at,
      'invalidated_at', v_result.invalidated_at
    ) end
  );
end;
$$;

revoke all on function public.get_agentic_rectification_case_dossier(uuid, uuid)
  from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_case_dossier(uuid, uuid)
  to service_role;

commit;
