-- ============================================================================
-- i18n readiness: per-user locale column + error codes in triggers/RPCs.
-- Clients map codes to localized strings via packages/shared/src/i18n/error-map.
-- Keep codes stable — they are the public API between DB and clients.
-- ============================================================================

-- ----------------------------------------------------------------------------
-- profiles.locale
-- ----------------------------------------------------------------------------
alter table public.profiles
  add column if not exists locale text not null default 'en';

alter table public.profiles
  drop constraint if exists profiles_locale_supported;

alter table public.profiles
  add constraint profiles_locale_supported check (locale in ('en', 'de'));

-- ----------------------------------------------------------------------------
-- Rewrite handle_new_user to:
--   * raise error codes instead of prose
--   * accept optional `locale` in user_metadata
-- ----------------------------------------------------------------------------
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
declare
  v_invite_code  text;
  v_username     text;
  v_display_name text;
  v_locale       text;
  v_invite       public.invites%rowtype;
begin
  v_invite_code  := new.raw_user_meta_data->>'invite_code';
  v_username     := lower(trim(new.raw_user_meta_data->>'username'));
  v_display_name := nullif(trim(new.raw_user_meta_data->>'display_name'), '');
  v_locale       := lower(trim(new.raw_user_meta_data->>'locale'));

  if v_display_name is null then
    v_display_name := v_username;
  end if;
  if v_locale is null or v_locale not in ('en', 'de') then
    v_locale := 'en';
  end if;

  if v_invite_code is null or length(v_invite_code) = 0 then
    raise exception 'ERR_INVITE_CODE_REQUIRED';
  end if;

  if v_username is null or v_username !~ '^[a-z0-9_]{3,32}$' then
    raise exception 'ERR_USERNAME_INVALID';
  end if;

  if not coalesce((select (value)::boolean from public.admin_settings where key = 'invites_enabled'), true) then
    raise exception 'ERR_INVITES_DISABLED';
  end if;

  select * into v_invite from public.invites
   where code = v_invite_code
     for update;

  if not found then
    raise exception 'ERR_INVITE_NOT_FOUND';
  end if;

  if v_invite.disabled then
    raise exception 'ERR_INVITE_DISABLED';
  end if;

  if v_invite.expires_at is not null and v_invite.expires_at < now() then
    raise exception 'ERR_INVITE_EXPIRED';
  end if;

  if v_invite.uses_limit is not null and v_invite.uses_count >= v_invite.uses_limit then
    raise exception 'ERR_INVITE_EXHAUSTED';
  end if;

  update public.invites
     set uses_count = uses_count + 1
   where code = v_invite.code;

  insert into public.profiles (user_id, username, display_name, locale)
  values (new.id, v_username, v_display_name, v_locale);

  return new;
end;
$$;

-- ----------------------------------------------------------------------------
-- messages_update_guard with error codes.
-- ----------------------------------------------------------------------------
create or replace function public.messages_update_guard()
returns trigger language plpgsql security definer set search_path = public as $$
declare
  caller uuid := auth.uid();
  is_mod boolean := public.is_conversation_mod_or_higher(new.conversation_id);
begin
  if old.deleted_at is not null then
    raise exception 'ERR_MESSAGE_DELETED';
  end if;

  if old.deleted_at is null and new.deleted_at is not null then
    if new.deleted_by is null then
      new.deleted_by := caller;
    end if;
    if not (old.sender_id = caller or is_mod) then
      raise exception 'ERR_DELETE_FORBIDDEN';
    end if;
    return new;
  end if;

  if old.sender_id <> caller then
    raise exception 'ERR_EDIT_NOT_SENDER';
  end if;

  if now() - old.created_at > interval '24 hours' then
    raise exception 'ERR_EDIT_WINDOW_EXPIRED';
  end if;

  new.edited_at := now();
  new.conversation_id := old.conversation_id;
  new.sender_id       := old.sender_id;
  new.created_at      := old.created_at;

  return new;
end;
$$;

-- ----------------------------------------------------------------------------
-- envelopes_update_guard with error codes.
-- ----------------------------------------------------------------------------
create or replace function public.envelopes_update_guard()
returns trigger language plpgsql security definer set search_path = public as $$
declare
  caller uuid := auth.uid();
  msg_created timestamptz;
  msg_sender uuid;
begin
  select created_at, sender_id into msg_created, msg_sender
  from public.messages where id = new.message_id;

  if msg_sender <> caller then
    raise exception 'ERR_ENVELOPE_NOT_SENDER';
  end if;

  if now() - msg_created > interval '24 hours' then
    raise exception 'ERR_ENVELOPE_WINDOW_EXPIRED';
  end if;

  return new;
end;
$$;

-- ----------------------------------------------------------------------------
-- friendships_update_guard with error codes.
-- ----------------------------------------------------------------------------
create or replace function public.friendships_update_guard()
returns trigger language plpgsql security definer set search_path = public as $$
declare
  caller uuid := auth.uid();
begin
  if old.status = 'pending' and new.status = 'accepted' then
    if caller = old.requested_by then
      raise exception 'ERR_FRIEND_SELF_ACCEPT';
    end if;
    new.accepted_at := now();
    return new;
  end if;

  if new.status = 'blocked' then
    return new;
  end if;

  if old.status = 'blocked' and new.status <> 'blocked' then
    return new;
  end if;

  raise exception 'ERR_FRIEND_BAD_TRANSITION';
end;
$$;

-- ----------------------------------------------------------------------------
-- RPCs with error codes.
-- ----------------------------------------------------------------------------
create or replace function public.create_dm(target_user_id uuid)
returns uuid language plpgsql security definer set search_path = public as $$
declare
  caller          uuid := auth.uid();
  existing_conv   uuid;
  new_conv        uuid;
  target_accepts  boolean;
  friends         boolean;
begin
  if caller is null then raise exception 'ERR_NOT_AUTH'; end if;
  if caller = target_user_id then raise exception 'ERR_DM_SELF'; end if;

  select c.id into existing_conv
  from public.conversations c
  join public.conversation_members m1 on m1.conversation_id = c.id and m1.user_id = caller
  join public.conversation_members m2 on m2.conversation_id = c.id and m2.user_id = target_user_id
  where c.type = 'dm'
  limit 1;

  if existing_conv is not null then
    return existing_conv;
  end if;

  friends := public.are_friends(caller, target_user_id);
  select allow_dms_from_strangers into target_accepts from public.profiles where user_id = target_user_id;

  if not friends and not coalesce(target_accepts, false) then
    raise exception 'ERR_DM_STRANGERS_DISABLED';
  end if;

  insert into public.conversations (type, created_by) values ('dm', caller) returning id into new_conv;

  insert into public.conversation_members (conversation_id, user_id, role, accepted)
  values
    (new_conv, caller, 'member', true),
    (new_conv, target_user_id, 'member', friends);

  return new_conv;
end;
$$;

create or replace function public.accept_dm(conversation_id uuid)
returns void language plpgsql security definer set search_path = public as $$
begin
  update public.conversation_members
     set accepted = true
   where conversation_members.conversation_id = accept_dm.conversation_id
     and user_id = auth.uid();
  if not found then raise exception 'ERR_NO_PENDING_DM'; end if;
end;
$$;

create or replace function public.redeem_group_invite(code text)
returns uuid language plpgsql security definer set search_path = public as $$
declare
  v_invite public.group_invites%rowtype;
begin
  select * into v_invite from public.group_invites where group_invites.code = redeem_group_invite.code for update;
  if not found then raise exception 'ERR_GROUP_INVITE_NOT_FOUND'; end if;
  if v_invite.disabled then raise exception 'ERR_GROUP_INVITE_DISABLED'; end if;
  if v_invite.expires_at is not null and v_invite.expires_at < now() then raise exception 'ERR_GROUP_INVITE_EXPIRED'; end if;
  if v_invite.uses_limit is not null and v_invite.uses_count >= v_invite.uses_limit then raise exception 'ERR_GROUP_INVITE_EXHAUSTED'; end if;

  insert into public.conversation_members (conversation_id, user_id, role, accepted)
  values (v_invite.conversation_id, auth.uid(), 'member', true)
  on conflict do nothing;

  update public.group_invites set uses_count = uses_count + 1 where group_invites.code = v_invite.code;

  return v_invite.conversation_id;
end;
$$;

create or replace function public.send_friend_request(target_user_id uuid)
returns void language plpgsql security definer set search_path = public as $$
declare
  lo uuid := least(auth.uid(), target_user_id);
  hi uuid := greatest(auth.uid(), target_user_id);
begin
  if auth.uid() is null then raise exception 'ERR_NOT_AUTH'; end if;
  if auth.uid() = target_user_id then raise exception 'ERR_FRIEND_SELF'; end if;

  insert into public.friendships (user_lo, user_hi, requested_by, status)
  values (lo, hi, auth.uid(), 'pending')
  on conflict (user_lo, user_hi) do nothing;
end;
$$;
