-- Append-only rectification inference ledger. Engine result rows stay immutable.
-- C/D answers insert a transition and overlay inference_state on read.
-- Business schema only; do not copy into frontend/db/migrations (BUG-127 / BUG-144).

begin;

create table if not exists public.agentic_rectification_inference_transitions (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  result_id uuid not null references public.agentic_rectification_results(id) on delete cascade,
  previous_revision integer not null check (previous_revision >= 0),
  revision integer not null check (revision > previous_revision),
  candidate_set_id text not null check (length(btrim(candidate_set_id)) > 0),
  probe_id text,
  semantic_key text,
  candidate_split_hash text,
  answer_class text check (answer_class is null or answer_class in ('yes', 'weak_yes', 'no', 'unsure')),
  raw_answer text,
  posterior_before jsonb not null default '{}'::jsonb check (jsonb_typeof(posterior_before) = 'object'),
  posterior_after jsonb not null default '{}'::jsonb check (jsonb_typeof(posterior_after) = 'object'),
  score_deltas jsonb not null default '{}'::jsonb check (jsonb_typeof(score_deltas) = 'object'),
  inference_state jsonb not null check (jsonb_typeof(inference_state) = 'object'),
  decision_state_fingerprint text not null check (decision_state_fingerprint ~ '^[0-9a-f]{64}$'),
  reason text not null check (reason in ('choice', 'supersede', 'already_answered')),
  idempotency_key text not null check (length(btrim(idempotency_key)) > 0),
  receipt_after jsonb not null check (jsonb_typeof(receipt_after) = 'object'),
  created_at timestamptz not null default pg_catalog.now(),
  unique (case_id, revision),
  unique (case_id, idempotency_key)
);

create index if not exists agentic_rectification_inference_transitions_case_revision_idx
  on public.agentic_rectification_inference_transitions (case_id, revision desc);

create index if not exists agentic_rectification_inference_transitions_result_idx
  on public.agentic_rectification_inference_transitions (result_id, revision desc);

alter table public.agentic_rectification_inference_transitions enable row level security;

revoke all on table public.agentic_rectification_inference_transitions
  from public, anon, authenticated;
grant all on table public.agentic_rectification_inference_transitions to service_role;

create or replace function public.compose_agentic_rectification_decision_receipt(
  p_case_id uuid,
  p_result_id uuid,
  p_receipt jsonb
)
returns jsonb
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
  v_transition public.agentic_rectification_inference_transitions%rowtype;
  v_receipt jsonb;
begin
  v_receipt := case
    when p_receipt is not null and jsonb_typeof(p_receipt) = 'object' then p_receipt
    else '{}'::jsonb
  end;
  if p_case_id is null or p_result_id is null then
    return v_receipt;
  end if;
  select * into v_transition
  from public.agentic_rectification_inference_transitions
  where case_id = p_case_id
    and result_id = p_result_id
  order by revision desc
  limit 1;
  if not found then
    return v_receipt;
  end if;
  return jsonb_set(
    jsonb_set(v_receipt, '{inference_state}', v_transition.inference_state, true),
    '{decision_state_fingerprint}',
    to_jsonb(v_transition.decision_state_fingerprint),
    true
  );
end;
$$;

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

create or replace function public.append_agentic_rectification_inference_transition(
  p_user_id uuid,
  p_case_id uuid,
  p_expected_revision integer,
  p_probe_id text,
  p_open_probe_id text,
  p_semantic_key text,
  p_candidate_split_hash text,
  p_answer_class text,
  p_raw_answer text,
  p_inference_state jsonb,
  p_posterior_before jsonb,
  p_posterior_after jsonb,
  p_score_deltas jsonb,
  p_decision_state_fingerprint text,
  p_reason text,
  p_idempotency_key text,
  p_candidate_set_id text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_result public.agentic_rectification_results%rowtype;
  v_existing public.agentic_rectification_inference_transitions%rowtype;
  v_current_revision integer;
  v_next_revision integer;
  v_receipt jsonb;
  v_id uuid;
  v_focus_probe_id text;
  v_last_probe_id text;
begin
  if p_user_id is null or p_case_id is null
    or p_expected_revision is null or p_expected_revision < 0
    or p_inference_state is null or jsonb_typeof(p_inference_state) <> 'object'
    or p_posterior_before is null or jsonb_typeof(p_posterior_before) <> 'object'
    or p_posterior_after is null or jsonb_typeof(p_posterior_after) <> 'object'
    or p_score_deltas is null or jsonb_typeof(p_score_deltas) <> 'object'
    or p_decision_state_fingerprint is null
    or p_decision_state_fingerprint !~ '^[0-9a-f]{64}$'
    or p_reason is null or p_reason not in ('choice', 'supersede', 'already_answered')
    or length(btrim(coalesce(p_idempotency_key, ''))) = 0
    or length(btrim(coalesce(p_candidate_set_id, ''))) = 0
    or p_answer_class is null or p_answer_class not in ('yes', 'weak_yes', 'no', 'unsure')
    or length(btrim(coalesce(p_probe_id, ''))) = 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;
  if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;

  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
  limit 1
  for update;
  if not found then
    raise exception 'agentic_rectification_result_not_found' using errcode = 'P0001';
  end if;
  if v_result.decision_receipt is null
    or jsonb_typeof(v_result.decision_receipt) <> 'object' then
    raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
  end if;

  select * into v_existing
  from public.agentic_rectification_inference_transitions
  where case_id = p_case_id
    and idempotency_key = p_idempotency_key;
  if found then
    v_receipt := public.compose_agentic_rectification_decision_receipt(
      p_case_id, v_result.id, v_result.decision_receipt
    );
    return jsonb_build_object(
      'transition_id', v_existing.id,
      'result_id', v_result.id,
      'revision', v_existing.revision,
      'idempotent', true,
      'decision_receipt', v_receipt,
      'decision_state_fingerprint', v_existing.decision_state_fingerprint
    );
  end if;

  select coalesce(max(revision), 0) into v_current_revision
  from public.agentic_rectification_inference_transitions
  where case_id = p_case_id;
  v_current_revision := greatest(
    v_current_revision,
    coalesce((v_result.decision_receipt -> 'inference_state' ->> 'revision')::integer, 0)
  );
  if p_expected_revision is distinct from v_current_revision then
    raise exception 'agentic_rectification_revision_conflict' using errcode = 'P0001';
  end if;

  if p_probe_id is distinct from nullif(btrim(coalesce(p_open_probe_id, '')), '')
    and p_reason is distinct from 'supersede' then
    raise exception 'agentic_rectification_stale_probe' using errcode = 'P0001';
  end if;

  select nullif(btrim(coalesce(v_focus.expected_answer_schema ->> 'probe_id', '')), '')
  into v_focus_probe_id
  from public.agentic_rectification_conversation_focuses v_focus
  where v_focus.case_id = p_case_id
    and v_focus.status = 'active'
  order by v_focus.asked_at desc, v_focus.id desc
  limit 1;
  if v_focus_probe_id is not null and p_probe_id is distinct from v_focus_probe_id then
    if p_reason is distinct from 'supersede' then
      raise exception 'agentic_rectification_stale_probe' using errcode = 'P0001';
    end if;
    select probe_id into v_last_probe_id
    from public.agentic_rectification_inference_transitions
    where case_id = p_case_id
    order by revision desc
    limit 1;
    if p_probe_id is distinct from v_last_probe_id then
      raise exception 'agentic_rectification_stale_probe' using errcode = 'P0001';
    end if;
  end if;

  v_next_revision := v_current_revision + 1;
  v_receipt := jsonb_set(
    jsonb_set(v_result.decision_receipt, '{inference_state}', p_inference_state, true),
    '{decision_state_fingerprint}',
    to_jsonb(p_decision_state_fingerprint),
    true
  );

  insert into public.agentic_rectification_inference_transitions (
    case_id, result_id, previous_revision, revision, candidate_set_id,
    probe_id, semantic_key, candidate_split_hash, answer_class, raw_answer,
    posterior_before, posterior_after, score_deltas, inference_state,
    decision_state_fingerprint, reason, idempotency_key, receipt_after
  ) values (
    p_case_id, v_result.id, p_expected_revision, v_next_revision, p_candidate_set_id,
    p_probe_id, nullif(btrim(coalesce(p_semantic_key, '')), ''),
    nullif(btrim(coalesce(p_candidate_split_hash, '')), ''),
    p_answer_class, nullif(btrim(coalesce(p_raw_answer, '')), ''),
    p_posterior_before, p_posterior_after, p_score_deltas, p_inference_state,
    p_decision_state_fingerprint, p_reason, p_idempotency_key, v_receipt
  ) returning id into v_id;

  return jsonb_build_object(
    'transition_id', v_id,
    'result_id', v_result.id,
    'revision', v_next_revision,
    'idempotent', false,
    'decision_receipt', v_receipt,
    'decision_state_fingerprint', p_decision_state_fingerprint
  );
end;
$$;

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

create or replace function public.get_agentic_rectification_latest_inference_transition(
  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;
  v_transition public.agentic_rectification_inference_transitions%rowtype;
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 * into v_transition
  from public.agentic_rectification_inference_transitions
  where case_id = p_case_id
  order by revision desc
  limit 1;
  if not found then
    return null;
  end if;
  return jsonb_build_object(
    'id', v_transition.id,
    'result_id', v_transition.result_id,
    'previous_revision', v_transition.previous_revision,
    'revision', v_transition.revision,
    'probe_id', v_transition.probe_id,
    'reason', v_transition.reason,
    'decision_state_fingerprint', v_transition.decision_state_fingerprint,
    'inference_state', v_transition.inference_state,
    'posterior_before', v_transition.posterior_before,
    'posterior_after', v_transition.posterior_after,
    'score_deltas', v_transition.score_deltas
  );
end;
$$;

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

create or replace function public.list_agentic_rectification_inference_transitions(
  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;
  v_rows jsonb;
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', item.id,
    'result_id', item.result_id,
    'previous_revision', item.previous_revision,
    'revision', item.revision,
    'probe_id', item.probe_id,
    'semantic_key', item.semantic_key,
    'candidate_split_hash', item.candidate_split_hash,
    'answer_class', item.answer_class,
    'reason', item.reason,
    'decision_state_fingerprint', item.decision_state_fingerprint,
    'inference_state', item.inference_state,
    'posterior_before', item.posterior_before,
    'posterior_after', item.posterior_after,
    'score_deltas', item.score_deltas
  ) order by item.revision), '[]'::jsonb)
  into v_rows
  from public.agentic_rectification_inference_transitions item
  where item.case_id = p_case_id;
  return v_rows;
end;
$$;

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

create or replace function public.patch_agentic_rectification_inference_state(
  p_user_id uuid,
  p_case_id uuid,
  p_inference_state jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
begin
  raise exception 'agentic_rectification_inference_patch_retired' using errcode = 'P0001';
end;
$$;

create or replace function public.persist_agentic_rectification_candidate_v2(
  p_user_id uuid,
  p_case_id uuid,
  p_engine_result_id text,
  p_evidence_ledger_fingerprint text,
  p_candidate_range_fingerprint text,
  p_skill_version text,
  p_algorithm_version text,
  p_event_contract_version text,
  p_decision_policy_version text,
  p_candidate_range jsonb,
  p_candidates jsonb,
  p_decision_receipt jsonb,
  p_execution_ledger jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_cached public.agentic_rectification_results%rowtype;
  v_snapshot jsonb;
  v_result_id uuid;
  v_candidate jsonb;
  v_candidate_id uuid;
  v_candidate_time time without time zone;
  v_candidate_time_text text;
  v_candidate_ordinal integer;
  v_saved_candidate jsonb;
  v_saved_candidates jsonb := '[]'::jsonb;
  v_seen_times text[] := array[]::text[];
  v_display_allowed boolean;
  v_selection_allowed boolean;
  v_confirmation_allowed boolean;
  v_overall_confidence text;
  v_margin_percent numeric;
  v_representative_time_text text;
  v_representative_time time without time zone;
  v_representative_candidate_id uuid;
  v_representative_count integer := 0;
  v_saved_decision_receipt jsonb;
begin
  if p_user_id is null or p_case_id is null
    or length(btrim(coalesce(p_engine_result_id, ''))) = 0
    or length(btrim(coalesce(p_evidence_ledger_fingerprint, ''))) = 0
    or length(btrim(coalesce(p_candidate_range_fingerprint, ''))) = 0
    or length(btrim(coalesce(p_skill_version, ''))) = 0
    or length(btrim(coalesce(p_algorithm_version, ''))) = 0
    or length(btrim(coalesce(p_event_contract_version, ''))) = 0
    or length(btrim(coalesce(p_decision_policy_version, ''))) = 0 then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;
  if p_candidate_range is null or jsonb_typeof(p_candidate_range) <> 'object' then
    raise exception 'agentic_rectification_invalid_candidate_range' using errcode = 'P0001';
  end if;
  if p_candidates is null or jsonb_typeof(p_candidates) <> 'array'
    or jsonb_array_length(p_candidates) = 0 then
    raise exception 'agentic_rectification_invalid_candidates' using errcode = 'P0001';
  end if;
  if p_decision_receipt is null or jsonb_typeof(p_decision_receipt) <> 'object'
    or jsonb_typeof(p_decision_receipt -> 'display_allowed') is distinct from 'boolean'
    or jsonb_typeof(p_decision_receipt -> 'accept_allowed') is distinct from 'boolean'
    or jsonb_typeof(p_decision_receipt -> 'confirm_allowed') is distinct from 'boolean'
    or jsonb_typeof(p_decision_receipt -> 'overall_confidence') is distinct from 'string' then
    raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
  end if;
  if p_execution_ledger is null or jsonb_typeof(p_execution_ledger) <> 'array' then
    raise exception 'agentic_rectification_invalid_execution_ledger' using errcode = 'P0001';
  end if;

  v_display_allowed := (p_decision_receipt ->> 'display_allowed')::boolean;
  v_selection_allowed := (p_decision_receipt ->> 'accept_allowed')::boolean;
  v_confirmation_allowed := (p_decision_receipt ->> 'confirm_allowed')::boolean;
  v_overall_confidence := p_decision_receipt ->> 'overall_confidence';
  v_saved_decision_receipt := p_decision_receipt - 'representative_candidate_id';
  if v_overall_confidence not in ('low', 'medium', 'high') then
    raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
  end if;
  if (p_decision_receipt ? 'margin_percent')
    and jsonb_typeof(p_decision_receipt -> 'margin_percent') not in ('number', 'null') then
    raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
  end if;
  v_margin_percent := case
    when jsonb_typeof(p_decision_receipt -> 'margin_percent') = 'number'
      then (p_decision_receipt ->> 'margin_percent')::numeric
    else null
  end;
  if v_selection_allowed and not v_display_allowed then
    raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
  end if;
  if v_confirmation_allowed and not v_selection_allowed then
    raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
  end if;

  v_representative_time_text := nullif(btrim(coalesce(p_decision_receipt ->> 'representative_time', '')), '');
  if v_representative_time_text is not null then
    if v_representative_time_text !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
      raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
    end if;
    v_representative_time := v_representative_time_text::time without time zone;
  end if;
  if v_confirmation_allowed and v_representative_time is null then
    raise exception 'agentic_rectification_invalid_decision_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 v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;
  if v_case.skill_version is distinct from p_skill_version then
    raise exception 'agentic_rectification_skill_version_mismatch' using errcode = 'P0001';
  end if;

  select * into v_cached
  from public.agentic_rectification_results
  where case_id = p_case_id
    and invalidated_at is null
    and evidence_ledger_fingerprint = p_evidence_ledger_fingerprint
    and candidate_range_fingerprint = p_candidate_range_fingerprint
    and skill_version = p_skill_version
    and algorithm_version = p_algorithm_version
    and event_contract_version = p_event_contract_version
    and decision_policy_version = p_decision_policy_version
  order by created_at desc
  limit 1;

  if found then
    return jsonb_build_object(
      'result_id', v_cached.id,
      'cached', true,
      'candidates', v_cached.candidates,
      'overall_confidence', v_cached.overall_confidence,
      'margin_percent', v_cached.margin_percent,
      'display_allowed', v_cached.display_allowed,
      'selection_allowed', v_cached.selection_allowed,
      'confirmation_allowed', v_cached.confirmation_allowed,
      'representative_time', v_cached.representative_time,
      'algorithm_version', v_cached.algorithm_version,
      'event_contract_version', v_cached.event_contract_version,
      'decision_policy_version', v_cached.decision_policy_version,
      'decision_receipt', public.compose_agentic_rectification_decision_receipt(p_case_id, v_cached.id, v_cached.decision_receipt),
      'execution_ledger', v_cached.execution_ledger
    );
  end if;

  v_snapshot := v_case.baseline_birth_snapshot;

  insert into public.agentic_rectification_results (
    user_id, session_id, case_id,
    engine_result_id, canonical_input_hash, algorithm_version,
    evidence_ledger_fingerprint, candidate_range_fingerprint, skill_version,
    event_contract_version, decision_policy_version,
    candidate_range, candidates,
    overall_confidence, margin_percent,
    display_allowed, selection_allowed, confirmation_allowed, representative_time,
    decision_receipt, execution_ledger,
    baseline_birth_date, baseline_reported_birth_time, baseline_active_birth_time,
    baseline_birth_time_source, baseline_birth_time_period,
    baseline_uncertainty_before_minutes, baseline_uncertainty_after_minutes,
    baseline_latitude, baseline_longitude, baseline_timezone_offset
  ) values (
    v_case.user_id, v_case.session_id, v_case.id,
    p_engine_result_id, p_evidence_ledger_fingerprint, p_algorithm_version,
    p_evidence_ledger_fingerprint, p_candidate_range_fingerprint, p_skill_version,
    p_event_contract_version, p_decision_policy_version,
    p_candidate_range, '[]'::jsonb,
    v_overall_confidence, v_margin_percent,
    v_display_allowed, v_selection_allowed, v_confirmation_allowed, v_representative_time,
    v_saved_decision_receipt, p_execution_ledger,
    (v_snapshot ->> 'birth_date')::date,
    (v_snapshot ->> 'reported_birth_time')::time without time zone,
    (v_snapshot ->> 'active_birth_time')::time without time zone,
    v_snapshot ->> 'birth_time_source',
    v_snapshot ->> 'birth_time_period',
    (v_snapshot ->> 'uncertainty_before_minutes')::integer,
    (v_snapshot ->> 'uncertainty_after_minutes')::integer,
    (v_snapshot ->> 'latitude')::double precision,
    (v_snapshot ->> 'longitude')::double precision,
    (v_snapshot ->> 'timezone_offset')::double precision
  ) returning id into v_result_id;

  for v_candidate, v_candidate_ordinal in
    select item.value, item.ordinality::integer
    from pg_catalog.jsonb_array_elements(p_candidates) with ordinality as item(value, ordinality)
  loop
    if jsonb_typeof(v_candidate) <> 'object' then
      raise exception 'agentic_rectification_invalid_candidates' using errcode = 'P0001';
    end if;
    v_candidate_time_text := nullif(btrim(coalesce(v_candidate ->> 'time', '')), '');
    if v_candidate_time_text is null
      or v_candidate_time_text !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
      or v_candidate_time_text = any(v_seen_times) then
      raise exception 'agentic_rectification_invalid_candidates' using errcode = 'P0001';
    end if;
    v_seen_times := pg_catalog.array_append(v_seen_times, v_candidate_time_text);
    v_candidate_time := v_candidate_time_text::time without time zone;
    v_candidate_id := gen_random_uuid();
    if v_representative_time is not null
      and v_candidate_time is not distinct from v_representative_time then
      v_representative_count := v_representative_count + 1;
      v_representative_candidate_id := v_candidate_id;
    end if;

    v_saved_candidate := jsonb_set(
      v_candidate - 'candidate_id',
      '{candidate_id}',
      to_jsonb(v_candidate_id::text),
      true
    );

    insert into public.agentic_rectification_candidates (
      id, result_id, user_id, case_id, ordinal,
      candidate_time, candidate_payload, is_representative
    ) values (
      v_candidate_id, v_result_id, p_user_id, p_case_id, v_candidate_ordinal,
      v_candidate_time, v_saved_candidate,
      v_representative_time is not null and v_candidate_time is not distinct from v_representative_time
    );
    v_saved_candidates := v_saved_candidates || jsonb_build_array(v_saved_candidate);
  end loop;

  if v_representative_time is not null and v_representative_count <> 1 then
    raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
  end if;
  if v_representative_candidate_id is not null then
    v_saved_decision_receipt := jsonb_set(
      v_saved_decision_receipt,
      '{representative_candidate_id}',
      to_jsonb(v_representative_candidate_id::text),
      true
    );
  end if;

  update public.agentic_rectification_results
  set candidates = v_saved_candidates,
      decision_receipt = v_saved_decision_receipt,
      updated_at = pg_catalog.now()
  where id = v_result_id;

  return jsonb_build_object(
    'result_id', v_result_id,
    'cached', false,
    'candidates', v_saved_candidates,
    'overall_confidence', v_overall_confidence,
    'margin_percent', v_margin_percent,
    'display_allowed', v_display_allowed,
    'selection_allowed', v_selection_allowed,
    'confirmation_allowed', v_confirmation_allowed,
    'representative_time', v_representative_time,
    'algorithm_version', p_algorithm_version,
    'event_contract_version', p_event_contract_version,
    'decision_policy_version', p_decision_policy_version,
    'decision_receipt', public.compose_agentic_rectification_decision_receipt(p_case_id, v_result_id, v_saved_decision_receipt),
    'execution_ledger', p_execution_ledger
  );
end;
$$;


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

create or replace function public.get_agentic_rectification_case(
  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_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 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_result
  from public.agentic_rectification_results
  where case_id = v_case.id
    and invalidated_at is null
  order by created_at desc
  limit 1;

  return 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,
    'created_at', v_case.created_at,
    'last_activity_at', v_case.last_activity_at,
    'completed_at', v_case.completed_at,
    'closed_reason', v_case.closed_reason,
    'evidence_count', v_evidence_count,
    'turn_count', v_turn_count,
    '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,
        'display_allowed', v_result.display_allowed,
        'selection_allowed', v_result.selection_allowed,
        'confirmation_allowed', v_result.confirmation_allowed,
        'representative_time', v_result.representative_time,
        'selected_candidate_id', v_result.selected_candidate_id,
        '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,
        'event_contract_version', v_result.event_contract_version,
        'decision_policy_version', v_result.decision_policy_version,
        'decision_receipt', public.compose_agentic_rectification_decision_receipt(v_case.id, v_result.id, v_result.decision_receipt),
        'execution_ledger', v_result.execution_ledger,
        'created_at', v_result.created_at,
        'invalidated_at', v_result.invalidated_at
      )
    end
  );
end;
$$;


revoke all on function public.get_agentic_rectification_case(uuid, uuid)
  from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_case(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,
      'display_allowed', v_result.display_allowed,
      'selection_allowed', v_result.selection_allowed,
      'confirmation_allowed', v_result.confirmation_allowed,
      'representative_time', v_result.representative_time,
      'selected_candidate_id', v_result.selected_candidate_id,
      '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,
      'event_contract_version', v_result.event_contract_version,
      'decision_policy_version', v_result.decision_policy_version,
      'decision_receipt', public.compose_agentic_rectification_decision_receipt(v_case.id, v_result.id, v_result.decision_receipt),
      'execution_ledger', v_result.execution_ledger,
      '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;
