-- Security hardening migration for OTP, public delivery invitations, and Edge Function rate limits.
-- Run this file once in Supabase SQL Editor before deploying the updated Edge Functions.

create extension if not exists pgcrypto;

create table if not exists public.rate_limits (
  id uuid primary key default gen_random_uuid(),
  scope text not null,
  rate_key text not null,
  window_start timestamptz not null,
  count integer not null default 1,
  blocked_until timestamptz,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  unique (scope, rate_key, window_start)
);

alter table public.delivery_invitations add column if not exists expires_at timestamptz;
alter table public.delivery_invitations add column if not exists revoked_at timestamptz;
alter table public.delivery_invitations add column if not exists access_count integer not null default 0;
alter table public.delivery_invitations add column if not exists last_accessed_at timestamptz;

create index if not exists idx_delivery_invitations_expires_at
on public.delivery_invitations (expires_at);

create index if not exists idx_rate_limits_scope_key_window
on public.rate_limits (scope, rate_key, window_start desc);

create index if not exists idx_rate_limits_blocked_until
on public.rate_limits (blocked_until);

create or replace function public.check_rate_limit(
  p_scope text,
  p_key text,
  p_max_count integer,
  p_window_seconds integer,
  p_block_seconds integer default 0
)
returns table (
  allowed boolean,
  current_count integer,
  limit_count integer,
  blocked_until timestamptz,
  window_start timestamptz
)
language plpgsql
security definer
set search_path = public
as $function$
declare
  v_now timestamptz := timezone('utc', now());
  v_window_start timestamptz;
  v_count integer;
  v_blocked_until timestamptz;
begin
  if p_max_count <= 0 then
    raise exception 'max_count must be positive';
  end if;

  if p_window_seconds <= 0 then
    raise exception 'window_seconds must be positive';
  end if;

  v_window_start := to_timestamp(
    floor(extract(epoch from v_now) / p_window_seconds) * p_window_seconds
  );

  select rl.blocked_until
  into v_blocked_until
  from public.rate_limits rl
  where rl.scope = p_scope
    and rl.rate_key = p_key
    and rl.blocked_until is not null
    and rl.blocked_until > v_now
  order by rl.blocked_until desc
  limit 1;

  if v_blocked_until is not null then
    return query
    select false, 0, p_max_count, v_blocked_until, v_window_start;
    return;
  end if;

  insert into public.rate_limits (scope, rate_key, window_start, count, blocked_until)
  values (p_scope, p_key, v_window_start, 1, null)
  on conflict (scope, rate_key, window_start)
  do update set
    count = public.rate_limits.count + 1,
    blocked_until = case
      when public.rate_limits.count + 1 > p_max_count and p_block_seconds > 0 then greatest(
        coalesce(public.rate_limits.blocked_until, v_now),
        v_now + make_interval(secs => p_block_seconds)
      )
      else public.rate_limits.blocked_until
    end,
    updated_at = v_now
  returning count, blocked_until
  into v_count, v_blocked_until;

  return query
  select
    v_count <= p_max_count and (v_blocked_until is null or v_blocked_until <= v_now),
    v_count,
    p_max_count,
    v_blocked_until,
    v_window_start;
end;
$function$;

alter table public.rate_limits enable row level security;

drop policy if exists "rate limits admin only" on public.rate_limits;
create policy "rate limits admin only" on public.rate_limits
for all
using (public.current_role_name() = 'admin')
with check (public.current_role_name() = 'admin');

drop policy if exists "users self or admin" on public.users;
create policy "users self or admin" on public.users
for select
using (public.current_role_name() = 'admin' or id = auth.uid());

