-- V9 Agentic Rectification runtime: durable Case / Evidence / Turn / Receipt
-- state plus safe open/entry-summary/get/close/upgrade RPCs and the one-time
-- idempotent legacy Agentic backfill.
--
-- Business schema only. This migration belongs in frontend/supabase/migrations
-- and MUST NOT be duplicated into frontend/db/migrations (identity foundation;
-- see BUG-127 / BUG-144).
--
-- Security model (same as the existing agentic_rectification_results):
--   * all new tables enable RLS and grant table access to service_role only;
--   * all RPCs are SECURITY DEFINER with search_path = '', granted to
--     service_role only; the browser never passes userId, birth snapshot,
--     candidate range or permission decisions -- the server derives them.

begin;

-- ---------------------------------------------------------------------------
-- 1. Cases
-- ---------------------------------------------------------------------------

create table if not exists public.agentic_rectification_cases (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id) on delete cascade,
  session_id uuid not null unique references public.chat_sessions(id) on delete cascade,
  status text not null default 'draft' check (
    status in (
      'draft',
      'collecting_evidence',
      'candidate_ready',
      'candidate_accepted',
      'needs_rebaseline',
      'paused',
      'confirmed',
      'closed',
      'abandoned',
      'superseded'
    )
  ),
  skill_name text not null default 'jyotish-birth-time-rectification'
    check (length(btrim(skill_name)) > 0),
  skill_version text not null check (length(btrim(skill_version)) > 0),
  baseline_profile_fingerprint text not null check (length(btrim(baseline_profile_fingerprint)) > 0),
  baseline_birth_snapshot jsonb not null check (jsonb_typeof(baseline_birth_snapshot) = 'object'),
  candidate_range jsonb not null check (jsonb_typeof(candidate_range) = 'object'),
  accepted_time time without time zone,
  confirmed_time time without time zone,
  created_at timestamptz not null default pg_catalog.now(),
  updated_at timestamptz not null default pg_catalog.now(),
  last_activity_at timestamptz not null default pg_catalog.now(),
  completed_at timestamptz,
  closed_reason text check (closed_reason is null or closed_reason in ('completed_by_user', 'abandoned_by_user', 'other')),
  check (status not in ('confirmed', 'closed') or completed_at is not null)
);

-- Resumable statuses: draft, collecting_evidence, candidate_ready,
-- candidate_accepted, needs_rebaseline, paused.
-- Terminal statuses: confirmed, closed, abandoned, superseded.
-- At most ONE resumable case per user. This partial unique index is the
-- database enforcement point; the service layer must never bypass it.
create unique index if not exists agentic_rectification_cases_one_resumable_per_user
  on public.agentic_rectification_cases (user_id)
  where status in (
    'draft', 'collecting_evidence', 'candidate_ready', 'candidate_accepted',
    'needs_rebaseline', 'paused'
  );

create index if not exists agentic_rectification_cases_user_activity_idx
  on public.agentic_rectification_cases (user_id, last_activity_at desc);

-- ---------------------------------------------------------------------------
-- 2. Turns (raw user/assistant text + model version; reasoning is forbidden)
-- ---------------------------------------------------------------------------

create table if not exists public.agentic_rectification_turns (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  user_message text,
  assistant_message text,
  status text not null check (status in ('pending', 'completed', 'failed', 'retryable')),
  model_name text not null check (length(btrim(model_name)) > 0),
  model_version text,
  created_at timestamptz not null default pg_catalog.now(),
  completed_at timestamptz,
  updated_at timestamptz not null default pg_catalog.now(),
  check (status <> 'completed' or (assistant_message is not null and length(btrim(assistant_message)) > 0))
);

create index if not exists agentic_rectification_turns_case_idx
  on public.agentic_rectification_turns (case_id, created_at);

-- ---------------------------------------------------------------------------
-- 3. Evidence (append-only revision lineage, server-owned IDs)
-- ---------------------------------------------------------------------------

create table if not exists public.agentic_rectification_evidence (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  source_turn_id uuid not null references public.agentic_rectification_turns(id) on delete cascade,
  source_message_id uuid,
  user_quote text not null check (length(btrim(user_quote)) > 0),
  subject text not null check (subject in ('self', 'family', 'other')),
  event_kind text not null check (
    event_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'
    )
  ),
  domain text not null check (
    domain in ('education', 'career', 'relationship', 'relocation', 'finance', 'health', 'family', 'other')
  ),
  occurred_from date,
  occurred_to date,
  date_precision text not null check (date_precision in ('year', 'month', 'day', 'range', 'unknown')),
  summary text not null check (length(btrim(summary)) > 0),
  status text not null default 'draft' check (
    status in ('draft', 'pending_confirmation', 'confirmed', 'superseded', 'rejected')
  ),
  supersedes_evidence_id uuid references public.agentic_rectification_evidence(id) on delete set null,
  created_at timestamptz not null default pg_catalog.now(),
  confirmed_at timestamptz,
  updated_at timestamptz not null default pg_catalog.now(),
  check (status <> 'confirmed' or confirmed_at is not null),
  check (supersedes_evidence_id is null or supersedes_evidence_id <> id)
);

create index if not exists agentic_rectification_evidence_case_idx
  on public.agentic_rectification_evidence (case_id, created_at);

create index if not exists agentic_rectification_evidence_lineage_idx
  on public.agentic_rectification_evidence (supersedes_evidence_id)
  where supersedes_evidence_id is not null;

-- ---------------------------------------------------------------------------
-- 4. Tool receipts (fingerprints/phase/tool/status only; no payload/reasoning)
-- ---------------------------------------------------------------------------

create table if not exists public.agentic_rectification_tool_receipts (
  id uuid primary key default gen_random_uuid(),
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  turn_id uuid not null references public.agentic_rectification_turns(id) on delete cascade,
  tool_name text not null check (
    tool_name in (
      'rectification-read-case',
      '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'
    )
  ),
  public_phase text not null check (
    public_phase in (
      'run.started', 'skill.started', 'skill.loaded', 'case.loaded',
      'evidence.proposed', 'evidence.confirmed', 'candidates.comparing', 'candidates.updated',
      'diagnostics.completed', 'candidate.accepted', 'birth_time.confirmed',
      'answer.delta', 'run.completed', 'run.failed'
    )
  ),
  started_at timestamptz not null default pg_catalog.now(),
  completed_at timestamptz,
  status text not null check (status in ('started', 'completed', 'failed', 'skipped')),
  input_fingerprint text,
  result_fingerprint text,
  engine_version text,
  safe_error_code text
);

create index if not exists agentic_rectification_tool_receipts_turn_idx
  on public.agentic_rectification_tool_receipts (case_id, turn_id);

-- ---------------------------------------------------------------------------
-- 5. Open request idempotency ledger (double-click / multi-tab safe)
-- ---------------------------------------------------------------------------

create table if not exists public.agentic_rectification_open_ledger (
  request_id uuid not null,
  user_id uuid not null references auth.users(id) on delete cascade,
  case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
  session_id uuid not null references public.chat_sessions(id) on delete cascade,
  intent text not null check (intent in ('homepage', 'session', 'new')),
  created_at timestamptz not null default pg_catalog.now(),
  primary key (user_id, request_id)
);

-- ---------------------------------------------------------------------------
-- 6. Forward extension of agentic_rectification_results to case_id semantics
-- ---------------------------------------------------------------------------

alter table public.agentic_rectification_results
  add column if not exists case_id uuid
    references public.agentic_rectification_cases(id) on delete set null,
  add column if not exists evidence_ledger_fingerprint text,
  add column if not exists candidate_range_fingerprint text,
  add column if not exists skill_version text;

create index if not exists agentic_rectification_results_case_idx
  on public.agentic_rectification_results (case_id)
  where case_id is not null;

-- ---------------------------------------------------------------------------
-- 7. Chat session reverse pointer (one-to-one, bidirectional consistency)
-- ---------------------------------------------------------------------------

alter table public.chat_sessions
  add column if not exists agentic_rectification_case_id uuid
    references public.agentic_rectification_cases(id) on delete set null;

create unique index if not exists chat_sessions_agentic_rectification_case_unique
  on public.chat_sessions (agentic_rectification_case_id)
  where agentic_rectification_case_id is not null;

-- ---------------------------------------------------------------------------
-- 8. RLS + grants (service-role only; browser never touches these tables)
-- ---------------------------------------------------------------------------

alter table public.agentic_rectification_cases enable row level security;
alter table public.agentic_rectification_turns enable row level security;
alter table public.agentic_rectification_evidence enable row level security;
alter table public.agentic_rectification_tool_receipts enable row level security;
alter table public.agentic_rectification_open_ledger enable row level security;

revoke all on table public.agentic_rectification_cases from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_turns from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_evidence from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_tool_receipts from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_open_ledger from public, anon, authenticated, service_role;
grant all on table public.agentic_rectification_cases to service_role;
grant all on table public.agentic_rectification_turns to service_role;
grant all on table public.agentic_rectification_evidence to service_role;
grant all on table public.agentic_rectification_tool_receipts to service_role;
grant all on table public.agentic_rectification_open_ledger to service_role;

-- ---------------------------------------------------------------------------
-- 9. Bidirectional Case <-> Session consistency triggers
-- ---------------------------------------------------------------------------

create or replace function public.agentic_rectification_cases_sync_session()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  if new.session_id is distinct from old.session_id then
    update public.chat_sessions
    set agentic_rectification_case_id = null
    where agentic_rectification_case_id = old.id;
  end if;
  update public.chat_sessions
  set agentic_rectification_case_id = new.id,
      session_type = 'birth_time_rectification'
  where id = new.session_id;
  return new;
end;
$$;

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

drop trigger if exists agentic_rectification_cases_sync_session_trigger
  on public.agentic_rectification_cases;
create trigger agentic_rectification_cases_sync_session_trigger
after insert or update of session_id on public.agentic_rectification_cases
for each row execute function public.agentic_rectification_cases_sync_session();

create or replace function public.agentic_rectification_chat_sessions_case_guard()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
begin
  if new.agentic_rectification_case_id is not null then
    select * into v_case
    from public.agentic_rectification_cases
    where id = new.agentic_rectification_case_id;
    if not found then
      raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
    end if;
    if v_case.session_id is distinct from new.id then
      raise exception 'agentic_rectification_case_session_mismatch' using errcode = 'P0001';
    end if;
    if v_case.user_id is distinct from new.user_id then
      raise exception 'agentic_rectification_case_owner_mismatch' using errcode = 'P0001';
    end if;
  end if;
  return new;
end;
$$;

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

drop trigger if exists agentic_rectification_chat_sessions_case_guard_trigger
  on public.chat_sessions;
create trigger agentic_rectification_chat_sessions_case_guard_trigger
before update of agentic_rectification_case_id on public.chat_sessions
for each row execute function public.agentic_rectification_chat_sessions_case_guard();

-- ---------------------------------------------------------------------------
-- 10. Shared helpers used by RPCs
-- ---------------------------------------------------------------------------

create or replace function public.agentic_rectification_normalize_quote(p_value text)
returns text
language sql
immutable
as $$
  select regexp_replace(
    lower(coalesce(p_value, '')),
    '[\s\u3000，。！？、；：“”‘’（）《》·—…]',
    '',
    'g'
  )
$$;

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

create or replace function public.agentic_rectification_resumable_statuses()
returns text[]
language sql
immutable
as $$
  select array[
    'draft', 'collecting_evidence', 'candidate_ready', 'candidate_accepted',
    'needs_rebaseline', 'paused'
  ]::text[]
$$;

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

create or replace function public.agentic_rectification_legacy_fingerprint(
  p_user_id uuid,
  p_session_id uuid,
  p_status text
)
returns text
language sql
immutable
as $$
  select encode(
    public.digest(
      convert_to(
        p_user_id::text || ':' || p_session_id::text || ':' || coalesce(p_status, ''),
        'utf8'
      ),
      'sha256'
    ),
    'hex'
  )
$$;

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

-- ---------------------------------------------------------------------------
-- 11. Open Case RPC (idempotent, same-user serialized, atomic case+session)
-- ---------------------------------------------------------------------------

create or replace function public.open_agentic_rectification_case(
  p_user_id uuid,
  p_request_id uuid,
  p_intent text,
  p_session_id uuid,
  p_skill_name text,
  p_skill_version text,
  p_baseline_profile_fingerprint text,
  p_baseline_birth_snapshot jsonb,
  p_candidate_range jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_session public.chat_sessions%rowtype;
  v_session_id uuid;
  v_turn_count bigint;
begin
  if p_user_id is null or p_request_id is null then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;
  if p_intent not in ('homepage', 'session', 'new') then
    raise exception 'agentic_rectification_invalid_intent' using errcode = 'P0001';
  end if;
  if length(btrim(p_skill_name)) = 0 or length(btrim(p_skill_version)) = 0
    or length(btrim(coalesce(p_baseline_profile_fingerprint, ''))) = 0 then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;

  -- Serialize concurrent open/create for the same user (double-click, two
  -- tabs, retries). The advisory lock is released when this transaction ends.
  perform pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtext('agentic_rectification_open:' || p_user_id::text)
  );

  -- Idempotency: an earlier open with the same requestId wins.
  select c.* into v_case
  from public.agentic_rectification_cases c
  join public.agentic_rectification_open_ledger l
    on l.case_id = c.id and l.session_id = c.session_id
  where l.user_id = p_user_id and l.request_id = p_request_id
  limit 1;

  if found then
    return jsonb_build_object(
      'disposition', case
        when v_case.status = any (public.agentic_rectification_resumable_statuses()) then 'resumed'
        else 'readonly'
      end,
      'case_id', v_case.id,
      'session_id', v_case.session_id,
      'status', v_case.status,
      'should_start_opening', false,
      'skill_version', v_case.skill_version
    );
  end if;

  if p_intent = 'session' then
    if p_session_id is null then
      raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
    end if;
    select * into v_session
    from public.chat_sessions
    where id = p_session_id and user_id = p_user_id;
    if not found then
      raise exception 'agentic_rectification_session_not_found' using errcode = 'P0001';
    end if;
    if v_session.session_type <> 'birth_time_rectification' then
      raise exception 'agentic_rectification_session_not_rectification' using errcode = 'P0001';
    end if;
    select * into v_case
    from public.agentic_rectification_cases
    where session_id = p_session_id;
    if not found then
      raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
    end if;
    return jsonb_build_object(
      'disposition', case
        when v_case.status = any (public.agentic_rectification_resumable_statuses()) then 'resumed'
        else 'readonly'
      end,
      'case_id', v_case.id,
      'session_id', v_case.session_id,
      'status', v_case.status,
      'should_start_opening', false,
      'skill_version', v_case.skill_version
    );
  end if;

  if p_intent = 'homepage' then
    select * into v_case
    from public.agentic_rectification_cases
    where user_id = p_user_id
      and status = any (public.agentic_rectification_resumable_statuses())
    order by last_activity_at desc, id
    limit 1;
    if found then
      select count(*) into v_turn_count
      from public.agentic_rectification_turns
      where case_id = v_case.id;
      return jsonb_build_object(
        'disposition', 'resumed',
        'case_id', v_case.id,
        'session_id', v_case.session_id,
        'status', v_case.status,
        'should_start_opening', false,
        'skill_version', v_case.skill_version
      );
    end if;
  end if;

  if p_intent = 'new' then
    select * into v_case
    from public.agentic_rectification_cases
    where user_id = p_user_id
      and status = any (public.agentic_rectification_resumable_statuses())
    order by last_activity_at desc, id
    limit 1;
    if found then
      -- supersedeActive=true is forbidden: restarting with an active case
      -- must surface a safe conflict, never silently abandon.
      raise exception 'agentic_rectification_active_case_conflict' using errcode = 'P0001';
    end if;
  end if;

  -- Create a new case + a new session atomically. The inner exception block
  -- rolls back both inserts together if a concurrent request already created
  -- a resumable case for this user. Profile snapshot and candidate range are
  -- validated here because only this path persists them.
  begin
    if p_baseline_birth_snapshot is null or jsonb_typeof(p_baseline_birth_snapshot) <> 'object'
      or p_baseline_birth_snapshot ->> 'birth_date' is null
      or p_baseline_birth_snapshot ->> 'latitude' is null
      or p_baseline_birth_snapshot ->> 'longitude' is null
      or p_baseline_birth_snapshot ->> 'timezone_offset' is null
      or length(btrim(coalesce(p_baseline_birth_snapshot ->> 'birth_time_source', ''))) = 0 then
      raise exception 'agentic_rectification_profile_incomplete' using errcode = 'P0001';
    end if;
    if p_candidate_range is null or jsonb_typeof(p_candidate_range) <> 'object'
      or not (p_candidate_range ->> 'start_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
      or not (p_candidate_range ->> 'end_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
      raise exception 'agentic_rectification_invalid_range' using errcode = 'P0001';
    end if;

    insert into public.chat_sessions (user_id, title, theme, session_type, messages)
    values (p_user_id, '生时校正', 'general', 'birth_time_rectification', '[]'::jsonb)
    returning id into v_session_id;

    insert into public.agentic_rectification_cases (
      user_id, session_id, status, skill_name, skill_version,
      baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range
    ) values (
      p_user_id, v_session_id, 'draft', p_skill_name, p_skill_version,
      p_baseline_profile_fingerprint, p_baseline_birth_snapshot, p_candidate_range
    ) returning id into v_case.id;

    insert into public.agentic_rectification_open_ledger (
      request_id, user_id, case_id, session_id, intent
    ) values (
      p_request_id, p_user_id, v_case.id, v_session_id, p_intent
    );
  exception when unique_violation then
    select * into v_case
    from public.agentic_rectification_cases
    where user_id = p_user_id
      and status = any (public.agentic_rectification_resumable_statuses())
    order by last_activity_at desc, id
    limit 1;
    if not found then
      raise;
    end if;
    insert into public.agentic_rectification_open_ledger (
      request_id, user_id, case_id, session_id, intent
    ) values (
      p_request_id, p_user_id, v_case.id, v_case.session_id, p_intent
    );
    return jsonb_build_object(
      'disposition', 'resumed',
      'case_id', v_case.id,
      'session_id', v_case.session_id,
      'status', v_case.status,
      'should_start_opening', false,
      'skill_version', v_case.skill_version
    );
  end;

  return jsonb_build_object(
    'disposition', 'created',
    'case_id', v_case.id,
    'session_id', v_session_id,
    'status', 'draft',
    'should_start_opening', true,
    'skill_version', p_skill_version
  );
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 12. Entry summary RPC (homepage CTA truth)
-- ---------------------------------------------------------------------------

create or replace function public.get_agentic_rectification_entry_summary(
  p_user_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_resumable public.agentic_rectification_cases%rowtype;
  v_terminal public.agentic_rectification_cases%rowtype;
begin
  if p_user_id is null then
    raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
  end if;

  select * into v_resumable
  from public.agentic_rectification_cases
  where user_id = p_user_id
    and status = any (public.agentic_rectification_resumable_statuses())
  order by last_activity_at desc, id
  limit 1;

  select * into v_terminal
  from public.agentic_rectification_cases
  where user_id = p_user_id
    and status in ('confirmed', 'closed', 'abandoned', 'superseded')
  order by last_activity_at desc, id
  limit 1;

  return jsonb_build_object(
    'has_resumable_case', v_resumable.id is not null,
    'has_terminal_case_with_time', v_terminal.id is not null
      and (v_terminal.accepted_time is not null or v_terminal.confirmed_time is not null),
    'latest_resumable', case
      when v_resumable.id is null then null
      else jsonb_build_object(
        'case_id', v_resumable.id,
        'status', v_resumable.status,
        'last_activity_at', v_resumable.last_activity_at
      )
    end,
    'latest_terminal', case
      when v_terminal.id is null then null
      else jsonb_build_object(
        'case_id', v_terminal.id,
        'status', v_terminal.status,
        'has_usable_time', v_terminal.accepted_time is not null or v_terminal.confirmed_time is not null
      )
    end
  );
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 13. Case read RPC (sanitized projection; never the birth snapshot)
-- ---------------------------------------------------------------------------

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,
        '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,
        'created_at', v_result.created_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;

-- ---------------------------------------------------------------------------
-- 14. Close Case RPC (terminal; never wraps as engine confirmed)
-- ---------------------------------------------------------------------------

create or replace function public.close_agentic_rectification_case(
  p_user_id uuid,
  p_case_id uuid,
  p_reason text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
begin
  if p_user_id is null or p_case_id is null
    or coalesce(p_reason, '') not in ('completed_by_user', 'abandoned_by_user', 'other') 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
    if v_case.status <> 'closed' then
      raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
    end if;
    return jsonb_build_object(
      'success', true, 'idempotent', true,
      'case_id', v_case.id, 'status', v_case.status
    );
  end if;

  update public.agentic_rectification_cases
  set status = 'closed',
      closed_reason = p_reason,
      completed_at = pg_catalog.now(),
      last_activity_at = pg_catalog.now(),
      updated_at = pg_catalog.now()
  where id = v_case.id;

  return jsonb_build_object(
    'success', true, 'idempotent', false,
    'case_id', v_case.id, 'status', 'closed'
  );
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 15. Upgrade Skill RPC (explicit migration; running cases pin their version)
-- ---------------------------------------------------------------------------

create or replace function public.upgrade_agentic_rectification_skill(
  p_user_id uuid,
  p_case_id uuid,
  p_skill_version text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_previous text;
begin
  if p_user_id is null or p_case_id is null
    or length(btrim(coalesce(p_skill_version, ''))) = 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;
  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;

  v_previous := v_case.skill_version;
  update public.agentic_rectification_cases
  set skill_version = p_skill_version,
      updated_at = pg_catalog.now()
  where id = v_case.id;

  return jsonb_build_object(
    'success', true,
    'case_id', v_case.id,
    'previous_skill_version', v_previous,
    'skill_version', p_skill_version,
    'idempotent', v_previous = p_skill_version
  );
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 16. Turn append RPC (server-owned; no reasoning ever persisted)
-- ---------------------------------------------------------------------------

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
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.agentic_rectification_cases%rowtype;
  v_turn_id uuid;
begin
  if p_user_id is null or p_case_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;
  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;

  insert into public.agentic_rectification_turns (
    case_id, user_message, assistant_message, status, model_name, model_version, completed_at
  ) values (
    p_case_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 id into v_turn_id;

  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);
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 17. Tool receipt insert RPC (fingerprints only)
-- ---------------------------------------------------------------------------

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
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_receipt_id uuid;
  v_turn_count bigint;
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;
  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 count(*) into v_turn_count
  from public.agentic_rectification_turns
  where id = p_turn_id and case_id = p_case_id;
  if v_turn_count = 0 then
    raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
  end if;

  insert into public.agentic_rectification_tool_receipts (
    case_id, turn_id, tool_name, public_phase, status,
    input_fingerprint, result_fingerprint, engine_version, safe_error_code,
    completed_at
  ) values (
    p_case_id, p_turn_id, p_tool_name, p_public_phase, p_status,
    p_input_fingerprint, p_result_fingerprint, p_engine_version, p_safe_error_code,
    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);
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 18. Evidence proposal / confirm / revise RPCs
-- ---------------------------------------------------------------------------

create or replace function public.propose_agentic_rectification_evidence(
  p_user_id uuid,
  p_case_id uuid,
  p_source_turn_id uuid,
  p_user_quote text,
  p_subject text,
  p_event_kind text,
  p_domain 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_turn public.agentic_rectification_turns%rowtype;
  v_evidence_id uuid;
  v_existing_id uuid;
begin
  if p_user_id is null or p_case_id is null or p_source_turn_id is null
    or length(btrim(coalesce(p_user_quote, ''))) = 0
    or length(btrim(coalesce(p_summary, ''))) = 0
    or p_subject not in ('self', 'family', 'other')
    or p_date_precision not in ('year', 'month', 'day', 'range', 'unknown') 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_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
    or position(
      public.agentic_rectification_normalize_quote(p_user_quote)
      in public.agentic_rectification_normalize_quote(v_turn.user_message)
    ) = 0 then
    raise exception 'agentic_rectification_quote_not_grounded' using errcode = 'P0001';
  end if;

  -- Idempotency: replaying the same proposal returns the existing draft.
  select id into v_existing_id
  from public.agentic_rectification_evidence
  where case_id = p_case_id
    and source_turn_id = p_source_turn_id
    and user_quote = p_user_quote
    and event_kind = p_event_kind
    and summary = p_summary
    and status in ('draft', 'pending_confirmation')
  limit 1;
  if v_existing_id is not null then
    return jsonb_build_object('evidence_id', v_existing_id, 'idempotent', true);
  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
  ) values (
    p_case_id, p_source_turn_id, p_user_quote, p_subject, p_event_kind, p_domain,
    p_occurred_from, p_occurred_to, p_date_precision, p_summary, 'draft'
  ) returning id into v_evidence_id;

  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('evidence_id', v_evidence_id, 'idempotent', false);
end;
$$;

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

create or replace function public.confirm_agentic_rectification_evidence(
  p_user_id uuid,
  p_case_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_evidence public.agentic_rectification_evidence%rowtype;
begin
  if p_user_id is null or p_case_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;
  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_evidence
  from public.agentic_rectification_evidence
  where id = p_evidence_id and case_id = p_case_id;
  if not found then
    raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
  end if;
  if v_evidence.status = 'confirmed' then
    return jsonb_build_object('evidence_id', v_evidence.id, 'status', 'confirmed', 'idempotent', true);
  end if;
  if v_evidence.status not in ('draft', 'pending_confirmation') then
    raise exception 'agentic_rectification_evidence_not_confirmable' using errcode = 'P0001';
  end if;

  update public.agentic_rectification_evidence
  set status = 'confirmed',
      confirmed_at = pg_catalog.now(),
      updated_at = pg_catalog.now()
  where id = v_evidence.id;

  return jsonb_build_object('evidence_id', v_evidence.id, 'status', 'confirmed', 'idempotent', false);
end;
$$;

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

create or replace function public.revise_agentic_rectification_evidence(
  p_user_id uuid,
  p_case_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_target public.agentic_rectification_evidence%rowtype;
  v_new_id uuid;
begin
  if p_user_id is null or p_case_id is null or p_evidence_id is null
    or length(btrim(coalesce(p_user_quote, ''))) = 0
    or length(btrim(coalesce(p_summary, ''))) = 0
    or p_date_precision not in ('year', 'month', 'day', 'range', 'unknown') 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_target
  from public.agentic_rectification_evidence
  where id = p_evidence_id and case_id = p_case_id;
  if not found then
    raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
  end if;
  if v_target.status not in ('confirmed', 'pending_confirmation') then
    raise exception 'agentic_rectification_evidence_not_revisable' using errcode = 'P0001';
  end if;

  -- Append-only lineage: the old row is superseded, never overwritten.
  update public.agentic_rectification_evidence
  set status = 'superseded',
      updated_at = pg_catalog.now()
  where id = v_target.id;

  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, supersedes_evidence_id
  ) values (
    v_target.case_id, v_target.source_turn_id, p_user_quote, v_target.subject,
    v_target.event_kind, v_target.domain,
    p_occurred_from, p_occurred_to, p_date_precision, p_summary,
    'pending_confirmation', v_target.id
  ) returning id into v_new_id;

  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(
    'evidence_id', v_new_id,
    'supersedes_evidence_id', v_target.id,
    'idempotent', false
  );
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 19. Legacy Agentic backfill (one-time, idempotent)
--
-- Mapping rules:
--   * engine_confirmed result            -> confirmed (terminal)
--   * user_accepted result only          -> candidate_accepted (resumable)
--   * messages present, no selection     -> collecting_evidence, or
--                                            candidate_ready when results exist
--   * empty session                      -> draft for the newest empty per user
--                                            only; older empties -> abandoned
--   * per user, at most one resumable    -> latest activity wins; the rest of
--                                            the resumable candidates are
--                                            superseded (read-only history kept)
--   * no confirmed evidence is ever generated from chat text
--   * agentic_rectification_results rows are mapped to the backfilled case_id
-- ---------------------------------------------------------------------------

create or replace function public.backfill_agentic_rectification_legacy_cases()
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_row record;
  v_case_id uuid;
  v_fingerprint text;
  v_snapshot jsonb;
  v_range jsonb;
  v_profile public.profiles%rowtype;
  v_selected_time time without time zone;
  v_result_range jsonb;
  v_base_birth_date date;
  v_base_lat double precision;
  v_base_lon double precision;
  v_base_tz double precision;
  v_base_source text;
  v_accepted_time time without time zone;
  v_confirmed_time time without time zone;
  v_legacy_sessions integer := 0;
  v_created integer := 0;
  v_mapped_results integer := 0;
  v_confirmed_count integer := 0;
  v_candidate_accepted_count integer := 0;
  v_candidate_ready_count integer := 0;
  v_collecting_count integer := 0;
  v_draft_count integer := 0;
  v_abandoned_count integer := 0;
  v_superseded_count integer := 0;
begin
  for v_row in
    with candidate as (
      select
        s.id as session_id,
        s.user_id,
        s.updated_at,
        (jsonb_typeof(coalesce(s.messages, '[]'::jsonb)) = 'array'
          and jsonb_array_length(coalesce(s.messages, '[]'::jsonb)) > 0) as has_messages,
        exists (
          select 1 from public.agentic_rectification_results r where r.session_id = s.id
        ) as has_results,
        (select r.selection_kind
         from public.agentic_rectification_results r
         where r.session_id = s.id
           and r.selection_kind in ('engine_confirmed', 'user_accepted')
         order by r.created_at desc
         limit 1) as result_kind,
        (select count(*) from public.agentic_rectification_results r where r.session_id = s.id) as result_count
      from public.chat_sessions s
      where s.session_type = 'birth_time_rectification'
        and not exists (
          select 1 from public.agentic_rectification_cases c where c.session_id = s.id
        )
    ),
    based as (
      select candidate.*,
        case
          when result_kind = 'engine_confirmed' then 'confirmed'
          when result_kind = 'user_accepted' then 'candidate_accepted'
          when has_messages then case when has_results then 'candidate_ready' else 'collecting_evidence' end
          else 'abandoned'
        end as base_status
      from candidate
    )
    select ranked.*,
      case
        when base_status = any (public.agentic_rectification_resumable_statuses())
             and resumable_rank > 1 then 'superseded'
        when base_status = 'abandoned'
             and resumable_rank = 1 and activity_rank = 1 then 'draft'
        else base_status
      end as final_status
    from (
      select based.*,
        row_number() over (
          partition by user_id
          order by (base_status = any (public.agentic_rectification_resumable_statuses())) desc,
                   updated_at desc, session_id
        ) as resumable_rank,
        row_number() over (
          partition by user_id order by updated_at desc, session_id
        ) as activity_rank
      from based
    ) ranked
    order by user_id, updated_at desc, session_id
  loop
    v_legacy_sessions := v_legacy_sessions + 1;
    v_range := null;
    v_accepted_time := null;
    v_confirmed_time := null;
    v_selected_time := null;
    v_result_range := null;
    v_base_birth_date := null;
    v_base_lat := null;
    v_base_lon := null;
    v_base_tz := null;
    v_base_source := null;

    select r.selected_time, r.candidate_range,
           r.baseline_birth_date, r.baseline_latitude, r.baseline_longitude,
           r.baseline_timezone_offset, r.baseline_birth_time_source
    into v_selected_time, v_result_range,
         v_base_birth_date, v_base_lat, v_base_lon, v_base_tz, v_base_source
    from public.agentic_rectification_results r
    where r.session_id = v_row.session_id
    order by r.created_at desc
    limit 1;

    if v_result_range is not null and jsonb_typeof(v_result_range) = 'object'
      and (v_result_range ->> 'start_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
      and (v_result_range ->> 'end_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
      v_range := v_result_range;
    end if;
    if v_row.final_status = 'confirmed' then
      v_confirmed_time := v_selected_time;
    elsif v_row.final_status = 'candidate_accepted' then
      v_accepted_time := v_selected_time;
    end if;

    select * into v_profile from public.profiles where id = v_row.user_id;

    if v_range is null then
      if v_profile.id is not null
        and (v_profile.active_birth_time is not null or v_profile.reported_birth_time is not null) then
        v_range := jsonb_build_object(
          'start_time', coalesce(to_char(v_profile.active_birth_time, 'HH24:MI'), to_char(v_profile.reported_birth_time, 'HH24:MI')),
          'end_time', coalesce(to_char(v_profile.active_birth_time, 'HH24:MI'), to_char(v_profile.reported_birth_time, 'HH24:MI'))
        );
      else
        v_range := jsonb_build_object('start_time', '00:00', 'end_time', '23:59');
      end if;
    end if;

    v_snapshot := jsonb_build_object(
      'birth_date', to_char(coalesce(v_profile.birth_date, v_base_birth_date), 'YYYY-MM-DD'),
      'latitude', coalesce(v_profile.latitude, v_base_lat),
      'longitude', coalesce(v_profile.longitude, v_base_lon),
      'timezone_offset', coalesce(v_profile.timezone_offset, v_base_tz),
      'birth_time_source', coalesce(v_profile.birth_time_source, v_base_source, 'legacy_import')
    );
    v_fingerprint := public.agentic_rectification_legacy_fingerprint(
      v_row.user_id, v_row.session_id, v_row.final_status
    );

    insert into public.agentic_rectification_cases (
      user_id, session_id, status, skill_name, skill_version,
      baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range,
      accepted_time, confirmed_time, last_activity_at, completed_at
    ) values (
      v_row.user_id, v_row.session_id, v_row.final_status,
      'jyotish-birth-time-rectification', '9.0.0',
      v_fingerprint, v_snapshot, v_range,
      v_accepted_time, v_confirmed_time, v_row.updated_at,
      case when v_row.final_status in ('confirmed', 'closed') then v_row.updated_at else null end
    ) returning id into v_case_id;

    update public.agentic_rectification_results
    set case_id = v_case_id
    where session_id = v_row.session_id and case_id is null;
    v_mapped_results := v_mapped_results + v_row.result_count;

    v_created := v_created + 1;
    if v_row.final_status = 'confirmed' then
      v_confirmed_count := v_confirmed_count + 1;
    elsif v_row.final_status = 'candidate_accepted' then
      v_candidate_accepted_count := v_candidate_accepted_count + 1;
    elsif v_row.final_status = 'candidate_ready' then
      v_candidate_ready_count := v_candidate_ready_count + 1;
    elsif v_row.final_status = 'collecting_evidence' then
      v_collecting_count := v_collecting_count + 1;
    elsif v_row.final_status = 'draft' then
      v_draft_count := v_draft_count + 1;
    elsif v_row.final_status = 'abandoned' then
      v_abandoned_count := v_abandoned_count + 1;
    elsif v_row.final_status = 'superseded' then
      v_superseded_count := v_superseded_count + 1;
    end if;
  end loop;

  return jsonb_build_object(
    'legacy_sessions_scanned', v_legacy_sessions,
    'cases_created', v_created,
    'results_mapped', v_mapped_results,
    'confirmed', v_confirmed_count,
    'candidate_accepted', v_candidate_accepted_count,
    'candidate_ready', v_candidate_ready_count,
    'collecting_evidence', v_collecting_count,
    'draft', v_draft_count,
    'abandoned', v_abandoned_count,
    'superseded', v_superseded_count
  );
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 20. Backfill verification (idempotent; can be re-run anytime)
-- ---------------------------------------------------------------------------

create or replace function public.verify_agentic_rectification_backfill()
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_total_cases bigint;
  v_resumable bigint;
  v_terminal bigint;
  v_unmapped_results bigint;
  v_legacy_without_case bigint;
  v_orphan_cases bigint;
  v_conflicts bigint;
begin
  select count(*) into v_total_cases from public.agentic_rectification_cases;
  select count(*) into v_resumable
  from public.agentic_rectification_cases
  where status = any (public.agentic_rectification_resumable_statuses());
  select count(*) into v_terminal
  from public.agentic_rectification_cases
  where status in ('confirmed', 'closed', 'abandoned', 'superseded');
  select count(*) into v_unmapped_results
  from public.agentic_rectification_results
  where case_id is null;
  select count(*) into v_legacy_without_case
  from public.chat_sessions s
  where s.session_type = 'birth_time_rectification'
    and not exists (
      select 1 from public.agentic_rectification_cases c where c.session_id = s.id
    );
  select count(*) into v_orphan_cases
  from public.agentic_rectification_cases c
  left join public.chat_sessions s on s.id = c.session_id
  where s.id is null;
  select count(*) into v_conflicts
  from (
    select user_id
    from public.agentic_rectification_cases
    where status = any (public.agentic_rectification_resumable_statuses())
    group by user_id
    having count(*) > 1
  ) conflicts;

  return jsonb_build_object(
    'total_cases', v_total_cases,
    'resumable_cases', v_resumable,
    'terminal_cases', v_terminal,
    'unmapped_results', v_unmapped_results,
    'legacy_sessions_without_case', v_legacy_without_case,
    'orphan_cases', v_orphan_cases,
    'resumable_conflicts', v_conflicts
  );
end;
$$;

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

-- ---------------------------------------------------------------------------
-- 21. Run the one-time backfill inside this migration (apply). The app never
--     runs bulk legacy migration at startup.
-- ---------------------------------------------------------------------------

do $$
declare
  v_result jsonb;
begin
  select public.backfill_agentic_rectification_legacy_cases() into v_result;
  raise notice 'agentic_rectification_v9 backfill: %', v_result;
end;
$$;

commit;
