begin;

create or replace function public.recover_conversational_rectification_orphan_reservations(
  p_user_id uuid,
  p_excluded_case_id uuid default null
)
returns integer
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_orphan public.birth_time_rectification_billing%rowtype;
  v_balance integer;
  v_recovery_action_id uuid;
  v_recovery_fingerprint text;
  v_recovery_response jsonb;
begin
  if p_user_id is null then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;
  perform pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtextextended(
      p_user_id::text || ':conversational-rectification-case',
      0
    )
  );
  select profile.credits into v_balance
  from public.profiles profile
  where profile.id = p_user_id
  for update;
  if not found then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;

  for v_orphan in
    select orphan_billing.*
    from public.birth_time_rectification_billing orphan_billing
    left join public.birth_time_rectification_cases orphan_case
      on orphan_case.id = orphan_billing.case_id
    where orphan_billing.user_id = p_user_id
      and (
        p_excluded_case_id is null
        or orphan_billing.case_id <> p_excluded_case_id
      )
      and orphan_billing.state = 'reserved'
      and orphan_case.id is null
    order by orphan_billing.reserved_at, orphan_billing.case_id
    for update of orphan_billing
  loop
    v_recovery_action_id :=
      public.conversational_rectification_billing_receipt_action_id(
        v_orphan.reserve_action_id,
        'recover_fee'
      );
    v_recovery_fingerprint := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
      pg_catalog.jsonb_build_object(
        'kind', 'recover_fee', 'userId', p_user_id, 'caseId', v_orphan.case_id,
        'expectedVersion', 0, 'actionId', v_recovery_action_id,
        'reserveActionId', v_orphan.reserve_action_id
      )::text,
      'UTF8'
    )), 'hex');

    update public.profiles profile
    set credits = profile.credits + v_orphan.price,
        updated_at = pg_catalog.now()
    where profile.id = p_user_id
    returning profile.credits into v_balance;
    if not found then
      raise exception 'conversational_billing_failed' using errcode = 'P0001';
    end if;

    insert into public.credit_transactions (
      user_id, transaction_type, amount, balance_after, request_id
    ) values (
      p_user_id, 'refund', v_orphan.price, v_balance,
      'rectification:' || v_orphan.case_id::text
    );

    update public.birth_time_rectification_billing orphan_billing
    set state = 'released',
        release_action_id = v_recovery_action_id,
        balance_after = v_balance,
        released_at = pg_catalog.now(),
        updated_at = pg_catalog.now()
    where orphan_billing.case_id = v_orphan.case_id
      and orphan_billing.user_id = p_user_id
      and orphan_billing.state = 'reserved';
    if not found then
      raise exception 'conversational_billing_failed' using errcode = 'P0001';
    end if;

    v_recovery_response := pg_catalog.jsonb_build_object(
      'success', true, 'credits', v_balance,
      'billing_state', 'released', 'error_code', null
    );
    insert into public.birth_time_rectification_action_receipts (
      case_id, action_id, user_id, action_kind, expected_turn_version,
      result_turn_version, request_fingerprint, request, response
    ) values (
      v_orphan.case_id, v_recovery_action_id, p_user_id, 'recover_fee', 0,
      0, v_recovery_fingerprint,
      public.conversational_rectification_action_request(
        'recover_fee', p_user_id, v_orphan.case_id, 0,
        v_recovery_action_id, v_recovery_fingerprint
      ),
      v_recovery_response
    );
  end loop;
  return v_balance;
end;
$$;

create or replace function public.reserve_conversational_rectification_fee(
  p_user_id uuid,
  p_case_id uuid,
  p_expected_version bigint,
  p_action_id uuid,
  p_price integer
)
returns table (
  success boolean,
  credits integer,
  billing_state text,
  error_code text
)
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_receipt public.birth_time_rectification_action_receipts%rowtype;
  v_billing public.birth_time_rectification_billing%rowtype;
  v_receipt_action_id uuid :=
    public.conversational_rectification_billing_receipt_action_id(
      p_action_id,
      'reserve_fee'
    );
  v_balance integer;
  v_response jsonb;
  v_fingerprint text := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
    pg_catalog.jsonb_build_object(
      'kind', 'reserve_fee', 'userId', p_user_id, 'caseId', p_case_id,
      'expectedVersion', p_expected_version, 'actionId', p_action_id,
      'price', p_price
    )::text,
    'UTF8'
  )), 'hex');
begin
  if p_user_id is null or p_case_id is null or p_action_id is null
    or p_expected_version is distinct from 0
    or p_price is null or not (p_price between 1 and 1000000) then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;
  if p_case_id is distinct from p_action_id then
    raise exception 'conversational_action_conflict' using errcode = 'P0001';
  end if;

  -- The account lock prevents two different start actions from reserving two
  -- fees before either action has created its case. The action lock preserves
  -- exact replay for the same request.
  perform pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtextextended(
      p_user_id::text || ':conversational-rectification-case',
      0
    )
  );
  perform pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
  );

  select r.* into v_receipt
  from public.birth_time_rectification_action_receipts r
  where r.case_id = p_case_id and r.action_id = v_receipt_action_id
  for update;
  if found then
    if v_receipt.user_id is distinct from p_user_id
      or v_receipt.action_kind is distinct from 'reserve_fee'
      or v_receipt.expected_turn_version is distinct from p_expected_version
      or v_receipt.request_fingerprint is distinct from v_fingerprint then
      raise exception 'conversational_action_conflict' using errcode = 'P0001';
    end if;
    return query select
      (v_receipt.response ->> 'success')::boolean,
      nullif(v_receipt.response ->> 'credits', '')::integer,
      nullif(v_receipt.response ->> 'billing_state', ''),
      nullif(v_receipt.response ->> 'error_code', '');
    return;
  end if;

  -- A public action identifies one start attempt even if a buggy caller loses
  -- its case id. Reusing that action for another case is a conflict, never a
  -- second debit.
  select r.* into v_receipt
  from public.birth_time_rectification_action_receipts r
  where r.user_id = p_user_id
    and r.action_id = v_receipt_action_id
    and r.action_kind = 'reserve_fee'
  for update;
  if found then
    raise exception 'conversational_action_conflict' using errcode = 'P0001';
  end if;

  perform 1
  from public.birth_time_rectification_cases active_case
  where active_case.user_id = p_user_id
    and active_case.id <> p_case_id
    and active_case.journey_protocol = 'conversational-evidence-v3'
    and active_case.status in ('starting', 'active', 'paused', 'confirming')
  for update;
  if found then
    raise exception 'conversational_action_conflict' using errcode = 'P0001';
  end if;

  -- Reservation intentionally precedes the external first-turn calculation,
  -- so it cannot share a transaction with case creation. A process/device
  -- loss in that gap leaves no case for the account resume RPC to expose.
  -- A fresh account-scoped start deterministically releases every such orphan
  -- under the same account/profile locks before it attempts another debit.
  v_balance := public.recover_conversational_rectification_orphan_reservations(
    p_user_id,
    p_case_id
  );

  select b.* into v_billing
  from public.birth_time_rectification_billing b
  where b.case_id = p_case_id
  for update;
  if found then
    raise exception 'conversational_action_conflict' using errcode = 'P0001';
  end if;

  if v_balance < p_price then
    v_response := pg_catalog.jsonb_build_object(
      'success', false, 'credits', v_balance,
      'billing_state', null, 'error_code', 'insufficient_credits'
    );
    insert into public.birth_time_rectification_action_receipts (
      case_id, action_id, user_id, action_kind, expected_turn_version,
      result_turn_version, request_fingerprint, request, response
    ) values (
      p_case_id, v_receipt_action_id, p_user_id, 'reserve_fee', 0,
      0, v_fingerprint,
      public.conversational_rectification_action_request(
        'reserve_fee', p_user_id, p_case_id, 0, p_action_id, v_fingerprint
      ),
      v_response
    );
    return query select false, v_balance, null::text, 'insufficient_credits'::text;
    return;
  end if;

  update public.profiles profile
  set credits = profile.credits - p_price,
      updated_at = pg_catalog.now()
  where profile.id = p_user_id and profile.credits >= p_price
  returning profile.credits into v_balance;
  if not found then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;

  insert into public.credit_transactions (
    user_id, transaction_type, amount, balance_after, request_id
  ) values (
    p_user_id, 'reserve', -p_price, v_balance,
    'rectification:' || p_case_id::text
  );

  insert into public.birth_time_rectification_billing (
    case_id, user_id, price, state, reservation_id, reserve_action_id,
    balance_after, reserved_at
  ) values (
    p_case_id, p_user_id, p_price, 'reserved',
    pg_catalog.gen_random_uuid(), p_action_id, v_balance, pg_catalog.now()
  );

  v_response := pg_catalog.jsonb_build_object(
    'success', true, 'credits', v_balance,
    'billing_state', 'reserved', 'error_code', null
  );
  insert into public.birth_time_rectification_action_receipts (
    case_id, action_id, user_id, action_kind, expected_turn_version,
    result_turn_version, request_fingerprint, request, response
  ) values (
    p_case_id, v_receipt_action_id, p_user_id, 'reserve_fee', 0,
    0, v_fingerprint,
    public.conversational_rectification_action_request(
      'reserve_fee', p_user_id, p_case_id, 0, p_action_id, v_fingerprint
    ),
    v_response
  );

  return query select true, v_balance, 'reserved'::text, null::text;
end;
$$;

create or replace function public.complete_conversational_rectification_fee(
  p_user_id uuid,
  p_case_id uuid,
  p_expected_version bigint,
  p_action_id uuid
)
returns table (
  success boolean,
  credits integer,
  billing_state text,
  error_code text
)
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.birth_time_rectification_cases%rowtype;
  v_receipt public.birth_time_rectification_action_receipts%rowtype;
  v_billing public.birth_time_rectification_billing%rowtype;
  v_receipt_action_id uuid :=
    public.conversational_rectification_billing_receipt_action_id(
      p_action_id,
      'complete_fee'
    );
  v_balance integer;
  v_response jsonb;
  v_success boolean;
  v_error_code text;
  v_state text;
  v_fingerprint text := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
    pg_catalog.jsonb_build_object(
      'kind', 'complete_fee', 'userId', p_user_id, 'caseId', p_case_id,
      'expectedVersion', p_expected_version, 'actionId', p_action_id
    )::text,
    'UTF8'
  )), 'hex');
begin
  if p_user_id is null or p_case_id is null or p_action_id is null
    or p_expected_version is null or p_expected_version < 0 then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;
  if p_case_id is distinct from p_action_id then
    raise exception 'conversational_action_conflict' using errcode = 'P0001';
  end if;

  perform pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
  );
  select c.* into v_case
  from public.birth_time_rectification_cases c
  where c.id = p_case_id and c.user_id = p_user_id
  for update;
  if not found or v_case.journey_protocol is distinct from 'conversational-evidence-v3' then
    raise exception 'conversational_case_not_found' using errcode = 'P0001';
  end if;

  select r.* into v_receipt
  from public.birth_time_rectification_action_receipts r
  where r.case_id = p_case_id and r.action_id = v_receipt_action_id
  for update;
  if found then
    if v_receipt.user_id is distinct from p_user_id
      or v_receipt.action_kind is distinct from 'complete_fee'
      or v_receipt.expected_turn_version is distinct from p_expected_version
      or v_receipt.request_fingerprint is distinct from v_fingerprint then
      raise exception 'conversational_action_conflict' using errcode = 'P0001';
    end if;
    return query select
      (v_receipt.response ->> 'success')::boolean,
      nullif(v_receipt.response ->> 'credits', '')::integer,
      nullif(v_receipt.response ->> 'billing_state', ''),
      nullif(v_receipt.response ->> 'error_code', '');
    return;
  end if;
  if v_case.turn_version is distinct from p_expected_version then
    raise exception 'conversational_stale_turn' using errcode = 'P0001';
  end if;

  select profile.credits into v_balance
  from public.profiles profile
  where profile.id = p_user_id
  for update;
  if not found then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;
  select b.* into v_billing
  from public.birth_time_rectification_billing b
  where b.case_id = p_case_id and b.user_id = p_user_id
  for update;

  if not found then
    v_success := false;
    v_state := null;
    v_error_code := 'billing_missing';
  elsif v_billing.state = 'charged' then
    v_success := true;
    v_state := 'charged';
    v_error_code := null;
  elsif v_billing.state = 'released' then
    v_success := false;
    v_state := 'released';
    v_error_code := 'reservation_released';
  elsif v_billing.state = 'migration_waived' then
    v_success := true;
    v_state := 'migration_waived';
    v_error_code := null;
  else
    update public.birth_time_rectification_billing
    set state = 'charged',
        billing_receipt_id = pg_catalog.gen_random_uuid(),
        complete_action_id = p_action_id,
        charged_at = pg_catalog.now(),
        updated_at = pg_catalog.now()
    where case_id = p_case_id and user_id = p_user_id and state = 'reserved';
    if not found then
      raise exception 'conversational_billing_failed' using errcode = 'P0001';
    end if;
    v_success := true;
    v_state := 'charged';
    v_error_code := null;
  end if;

  v_response := pg_catalog.jsonb_build_object(
    'success', v_success, 'credits', v_balance,
    'billing_state', v_state, 'error_code', v_error_code
  );
  insert into public.birth_time_rectification_action_receipts (
    case_id, action_id, user_id, action_kind, expected_turn_version,
    result_turn_version, request_fingerprint, request, response
  ) values (
    p_case_id, v_receipt_action_id, p_user_id, 'complete_fee', p_expected_version,
    v_case.turn_version, v_fingerprint,
    public.conversational_rectification_action_request(
      'complete_fee', p_user_id, p_case_id, p_expected_version,
      p_action_id, v_fingerprint
    ),
    v_response
  );
  return query select v_success, v_balance, v_state, v_error_code;
end;
$$;

create or replace function public.release_conversational_rectification_fee(
  p_user_id uuid,
  p_case_id uuid,
  p_expected_version bigint,
  p_action_id uuid,
  p_price integer
)
returns table (
  success boolean,
  credits integer,
  billing_state text,
  error_code text
)
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_case public.birth_time_rectification_cases%rowtype;
  v_receipt public.birth_time_rectification_action_receipts%rowtype;
  v_billing public.birth_time_rectification_billing%rowtype;
  v_receipt_action_id uuid :=
    public.conversational_rectification_billing_receipt_action_id(
      p_action_id,
      'release_fee'
    );
  v_balance integer;
  v_result_version bigint := p_expected_version;
  v_response jsonb;
  v_success boolean := true;
  v_error_code text;
  v_state text := 'released';
  v_fingerprint text := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
    pg_catalog.jsonb_build_object(
      'kind', 'release_fee', 'userId', p_user_id, 'caseId', p_case_id,
      'expectedVersion', p_expected_version, 'actionId', p_action_id,
      'price', p_price
    )::text,
    'UTF8'
  )), 'hex');
begin
  if p_user_id is null or p_case_id is null or p_action_id is null
    or p_expected_version is null or p_expected_version < 0
    or p_price is null or not (p_price between 1 and 1000000) then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;
  if p_case_id is distinct from p_action_id then
    raise exception 'conversational_action_conflict' using errcode = 'P0001';
  end if;

  perform pg_catalog.pg_advisory_xact_lock(
    pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
  );

  select r.* into v_receipt
  from public.birth_time_rectification_action_receipts r
  where r.case_id = p_case_id and r.action_id = v_receipt_action_id
  for update;
  if found then
    if v_receipt.user_id is distinct from p_user_id
      or v_receipt.action_kind is distinct from 'release_fee'
      or v_receipt.expected_turn_version is distinct from p_expected_version
      or v_receipt.request_fingerprint is distinct from v_fingerprint then
      raise exception 'conversational_action_conflict' using errcode = 'P0001';
    end if;
    return query select
      (v_receipt.response ->> 'success')::boolean,
      nullif(v_receipt.response ->> 'credits', '')::integer,
      nullif(v_receipt.response ->> 'billing_state', ''),
      nullif(v_receipt.response ->> 'error_code', '');
    return;
  end if;

  select c.* into v_case
  from public.birth_time_rectification_cases c
  where c.id = p_case_id
  for update;
  if found then
    if v_case.user_id is distinct from p_user_id
      or v_case.journey_protocol is distinct from 'conversational-evidence-v3' then
      raise exception 'conversational_case_not_found' using errcode = 'P0001';
    end if;
    if v_case.turn_version is distinct from p_expected_version then
      raise exception 'conversational_stale_turn' using errcode = 'P0001';
    end if;
    v_result_version := v_case.turn_version;
  elsif p_expected_version is distinct from 0 then
    raise exception 'conversational_stale_turn' using errcode = 'P0001';
  end if;

  select profile.credits into v_balance
  from public.profiles profile
  where profile.id = p_user_id
  for update;
  if not found then
    raise exception 'conversational_billing_failed' using errcode = 'P0001';
  end if;
  select b.* into v_billing
  from public.birth_time_rectification_billing b
  where b.case_id = p_case_id
  for update;

  if not found then
    insert into public.birth_time_rectification_billing (
      case_id, user_id, price, state, release_action_id,
      balance_after, released_at
    ) values (
      p_case_id, p_user_id, p_price, 'released', p_action_id,
      v_balance, pg_catalog.now()
    );
  elsif v_billing.user_id is distinct from p_user_id then
    raise exception 'conversational_case_not_found' using errcode = 'P0001';
  elsif v_billing.state = 'released' then
    v_state := 'released';
  elsif v_billing.state = 'charged' then
    v_success := false;
    v_state := 'charged';
    v_error_code := 'already_charged';
  elsif v_billing.state = 'migration_waived' then
    v_state := 'migration_waived';
  else
    update public.profiles profile
    set credits = profile.credits + v_billing.price,
        updated_at = pg_catalog.now()
    where profile.id = p_user_id
    returning profile.credits into v_balance;

    insert into public.credit_transactions (
      user_id, transaction_type, amount, balance_after, request_id
    ) values (
      p_user_id, 'refund', v_billing.price, v_balance,
      'rectification:' || p_case_id::text
    );

    update public.birth_time_rectification_billing
    set state = 'released',
        release_action_id = p_action_id,
        balance_after = v_balance,
        released_at = pg_catalog.now(),
        updated_at = pg_catalog.now()
    where case_id = p_case_id and user_id = p_user_id and state = 'reserved';
    if not found then
      raise exception 'conversational_billing_failed' using errcode = 'P0001';
    end if;

    -- Creation and fee settlement are separate calls. If settlement fails
    -- after the case was created, releasing the reservation must also close
    -- that unfinished case or it would block every later account-level start.
    if v_case.id is not null
      and v_case.status in ('starting', 'active', 'paused', 'confirming') then
      update public.birth_time_rectification_cases c
      set status = 'abandoned',
          turn_state = case
            when pg_catalog.jsonb_typeof(c.turn_state) = 'object'
              then pg_catalog.jsonb_set(
                c.turn_state, '{status}', pg_catalog.to_jsonb('abandoned'::text), true
              )
            else c.turn_state
          end,
          journey_snapshot = case
            when pg_catalog.jsonb_typeof(c.journey_snapshot) = 'object'
              then pg_catalog.jsonb_set(
                c.journey_snapshot, '{status}', pg_catalog.to_jsonb('abandoned'::text), true
              )
            else c.journey_snapshot
          end,
          updated_at = pg_catalog.now()
      where c.id = p_case_id
        and c.user_id = p_user_id
        and c.journey_protocol = 'conversational-evidence-v3'
        and c.turn_version = p_expected_version
        and c.status in ('starting', 'active', 'paused', 'confirming');
      if not found then
        raise exception 'conversational_stale_turn' using errcode = 'P0001';
      end if;
    end if;
  end if;

  v_response := pg_catalog.jsonb_build_object(
    'success', v_success, 'credits', v_balance,
    'billing_state', v_state, 'error_code', v_error_code
  );
  insert into public.birth_time_rectification_action_receipts (
    case_id, action_id, user_id, action_kind, expected_turn_version,
    result_turn_version, request_fingerprint, request, response
  ) values (
    p_case_id, v_receipt_action_id, p_user_id, 'release_fee', p_expected_version,
    v_result_version, v_fingerprint,
    public.conversational_rectification_action_request(
      'release_fee', p_user_id, p_case_id, p_expected_version,
      p_action_id, v_fingerprint
    ),
    v_response
  );
  return query select v_success, v_balance, v_state, v_error_code;
end;
$$;

revoke all on function public.reserve_conversational_rectification_fee(
  uuid, uuid, bigint, uuid, integer
) from public, anon, authenticated;
revoke all on function public.recover_conversational_rectification_orphan_reservations(
  uuid, uuid
) from public, anon, authenticated, service_role;
revoke all on function public.complete_conversational_rectification_fee(
  uuid, uuid, bigint, uuid
) from public, anon, authenticated;
revoke all on function public.release_conversational_rectification_fee(
  uuid, uuid, bigint, uuid, integer
) from public, anon, authenticated;

grant execute on function public.reserve_conversational_rectification_fee(
  uuid, uuid, bigint, uuid, integer
) to service_role;
grant execute on function public.complete_conversational_rectification_fee(
  uuid, uuid, bigint, uuid
) to service_role;
grant execute on function public.release_conversational_rectification_fee(
  uuid, uuid, bigint, uuid, integer
) to service_role;

commit;
