begin;

create or replace function public.agentic_rectification_evidence_kinds()
returns text[]
language sql
immutable
as $$
  select array[
    'education_start', 'education_completion', 'education_interruption',
    'education_change', 'education_milestone',
    'career_entry', 'career_change', 'promotion', 'career_pressure',
    'career_exit', 'business_start',
    'relationship_start', 'relationship_commitment', 'relationship_separation',
    'relationship_end', 'relationship_change',
    'relocation', 'foreign_move', 'return', 'home_change',
    'finance_gain', 'finance_loss', 'income_change', 'asset_change',
    'finance_change',
    'self_health_event', 'pressure_period', 'family_event', 'other'
  ]::text[]
$$;

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

create or replace function public.agentic_rectification_evidence_domains()
returns text[]
language sql
immutable
as $$
  select array[
    'education', 'career', 'relationship', 'relocation', 'finance',
    'health', 'health_pressure', 'family', 'other'
  ]::text[]
$$;

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

create or replace function public.agentic_rectification_date_precisions()
returns text[]
language sql
immutable
as $$
  select array['year', 'month', 'quarter', 'day', 'range', 'unknown']::text[]
$$;

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

create or replace function public.agentic_rectification_precision_rank(p_precision text)
returns integer
language sql
immutable
as $$
  select case p_precision
    when 'day' then 4
    when 'month' then 3
    when 'quarter' then 2
    when 'range' then 2
    when 'year' then 1
    when 'unknown' then 0
    else -1
  end
$$;

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

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

do $$
declare
  rec record;
begin
  for rec in
    select con.conname
    from pg_constraint con
    where con.conrelid = 'public.agentic_rectification_evidence'::regclass
      and con.contype = 'c'
      and pg_get_constraintdef(con.oid) like '%event_kind%'
  loop
    execute format(
      'alter table public.agentic_rectification_evidence drop constraint %I',
      rec.conname
    );
  end loop;

  for rec in
    select con.conname
    from pg_constraint con
    where con.conrelid = 'public.agentic_rectification_evidence'::regclass
      and con.contype = 'c'
      and pg_get_constraintdef(con.oid) like '%domain%'
      and pg_get_constraintdef(con.oid) not like '%event_kind%'
  loop
    execute format(
      'alter table public.agentic_rectification_evidence drop constraint %I',
      rec.conname
    );
  end loop;

  for rec in
    select con.conname
    from pg_constraint con
    where con.conrelid = 'public.agentic_rectification_evidence'::regclass
      and con.contype = 'c'
      and pg_get_constraintdef(con.oid) like '%date_precision%'
  loop
    execute format(
      'alter table public.agentic_rectification_evidence drop constraint %I',
      rec.conname
    );
  end loop;
end;
$$;

alter table public.agentic_rectification_evidence
  add constraint agentic_rectification_evidence_event_kind_check
    check (event_kind = any (public.agentic_rectification_evidence_kinds()));

alter table public.agentic_rectification_evidence
  add constraint agentic_rectification_evidence_domain_check
    check (domain = any (public.agentic_rectification_evidence_domains()));

alter table public.agentic_rectification_evidence
  add constraint agentic_rectification_evidence_date_precision_check
    check (date_precision = any (public.agentic_rectification_date_precisions()));

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') then
    return jsonb_build_object(
      'evidence_id', null,
      'idempotent', false,
      'outcome', 'rejected',
      'error_code', 'invalid_item',
      'status', 'rejected'
    );
  end if;
  if p_event_kind is null
    or not (p_event_kind = any (public.agentic_rectification_evidence_kinds()))
    or p_domain is null
    or not (p_domain = any (public.agentic_rectification_evidence_domains()))
    or p_date_precision is null
    or not (p_date_precision = any (public.agentic_rectification_date_precisions())) then
    return jsonb_build_object(
      'evidence_id', null,
      'idempotent', false,
      'outcome', 'rejected',
      'error_code', 'invalid_item',
      'status', 'rejected'
    );
  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
    return jsonb_build_object(
      'evidence_id', null,
      'idempotent', false,
      'outcome', 'rejected',
      'error_code', 'quote_not_grounded',
      'status', 'rejected'
    );
  end if;

  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,
      'outcome', 'accepted',
      'error_code', null,
      'status', 'draft'
    );
  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,
    'outcome', 'accepted',
    'error_code', null,
    'status', 'draft'
  );
end;
$$;

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

  if p_focus_id is null then
    v_result := public.confirm_agentic_rectification_evidence(
      p_user_id, p_case_id, p_evidence_id
    );
    return v_result || jsonb_build_object('focus_id', null);
  end if;

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

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

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;
  v_normalized text;
  v_has_day boolean;
  v_has_month boolean;
  v_has_year boolean;
  v_allows_coarser boolean;
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 is null
    or not (p_date_precision = any (public.agentic_rectification_date_precisions())) 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;

  v_normalized := public.agentic_rectification_normalize_quote(p_user_quote);
  if public.agentic_rectification_precision_rank(p_date_precision)
      < public.agentic_rectification_precision_rank(v_target.date_precision) then
    v_has_day := v_normalized ~ '[0-9]{1,2}月[0-9]{1,2}[日号]'
      or coalesce(p_user_quote, '') ~ '[0-9]{4}-[0-9]{2}-[0-9]{2}';
    v_has_month := v_normalized ~ '[0-9]{1,2}月'
      or coalesce(p_user_quote, '') ~ '[0-9]{4}-[0-9]{2}';
    v_has_year := v_normalized ~ '[0-9]{4}年'
      or coalesce(p_user_quote, '') ~ '[0-9]{4}';
    v_allows_coarser := case p_date_precision
      when 'year' then v_has_year and not v_has_month and not v_has_day
      when 'month' then v_has_month and not v_has_day
      when 'quarter' then v_has_month and not v_has_day
      when 'unknown' then v_normalized ~ '(不记得|记不清|不明|不知道)'
      else false
    end;
    if not v_allows_coarser then
      raise exception 'agentic_rectification_precision_downgrade' using errcode = 'P0001';
    end if;
  end if;

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

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

  select * into v_case
  from public.agentic_rectification_cases
  where id = p_case_id and user_id = p_user_id
  for update;
  if not found then
    raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
  end if;
  if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
    raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
  end if;

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

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

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

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

    if v_error is null and (
      length(v_item_key) = 0 or length(v_item_key) > 160
      or length(v_quote) = 0 or length(v_summary) = 0
      or v_subject not in ('self', 'family', 'other')
      or not (v_kind = any (public.agentic_rectification_evidence_kinds()))
      or not (v_domain = any (public.agentic_rectification_evidence_domains()))
      or not (v_precision = any (public.agentic_rectification_date_precisions()))
    ) then
      v_error := 'invalid_item';
    end if;

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

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

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

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

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

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

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

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

  if p_focus_id is not null then
    if v_focus_replay then
      if v_focus_match_count <> 1
        or v_focus_match_id is distinct from v_focus.target_evidence_id then
        raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
      end if;
      v_focus_resolution := 'idempotent';
    elsif v_focus_match_count = 1 then
      perform public.resolve_agentic_rectification_conversation_focus(
        p_user_id, p_case_id, p_focus_id, 'resolved', v_focus_match_id
      );
      v_focus_resolution := 'resolved';
    elsif v_focus_match_count > 1 then
      v_focus_resolution := 'ambiguous';
    else
      v_focus_resolution := 'unmatched';
    end if;
  end if;

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

commit;
