-- RIOTZ production schema + RLS policies for Supabase
-- Safe to run as a single migration.

begin;

create extension if not exists "pgcrypto";
create extension if not exists "citext";

-- =========================
-- Utility functions
-- =========================

create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $$
begin
  new.updated_at = timezone('utc', now());
  return new;
end;
$$;

-- =========================
-- Tables
-- =========================

create table if not exists public.admin_users (
  user_id uuid primary key references auth.users(id) on delete cascade,
  created_at timestamptz not null default timezone('utc', now())
);

create or replace function public.is_admin(_uid uuid)
returns boolean
language sql
stable
as $$
  select exists (
    select 1
    from public.admin_users au
    where au.user_id = _uid
  );
$$;

create table if not exists public.profiles (
  user_id uuid primary key references auth.users(id) on delete cascade,
  username citext not null unique,
  bio text not null default '',
  avatar_url text not null default '',
  banned boolean not null default false,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  constraint profiles_username_len check (char_length(username::text) between 3 and 32),
  constraint profiles_bio_len check (char_length(bio) <= 280)
);

create table if not exists public.posts (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id) on delete cascade,
  caption text not null default '',
  image_url text not null,
  likes_count integer not null default 0 check (likes_count >= 0),
  featured boolean not null default false,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  constraint posts_caption_len check (char_length(caption) <= 2800)
);

create table if not exists public.post_likes (
  post_id uuid not null references public.posts(id) on delete cascade,
  user_id uuid not null references auth.users(id) on delete cascade,
  created_at timestamptz not null default timezone('utc', now()),
  primary key (post_id, user_id)
);

create table if not exists public.comments (
  id uuid primary key default gen_random_uuid(),
  post_id uuid not null references public.posts(id) on delete cascade,
  user_id uuid not null references auth.users(id) on delete cascade,
  content text not null,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  constraint comments_content_len check (char_length(content) between 1 and 1200)
);

create table if not exists public.tracks (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id) on delete cascade,
  title text not null,
  audio_url text not null,
  tags text not null default '',
  featured boolean not null default false,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  constraint tracks_title_len check (char_length(title) between 1 and 120),
  constraint tracks_tags_len check (char_length(tags) <= 256)
);

create table if not exists public.chats (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  is_group boolean not null default true,
  created_by uuid not null references auth.users(id) on delete cascade,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  constraint chats_name_len check (char_length(name) between 1 and 120)
);

create table if not exists public.chat_members (
  chat_id uuid not null references public.chats(id) on delete cascade,
  user_id uuid not null references auth.users(id) on delete cascade,
  role text not null default 'member',
  joined_at timestamptz not null default timezone('utc', now()),
  primary key (chat_id, user_id),
  constraint chat_members_role_valid check (role in ('owner', 'admin', 'member'))
);

create table if not exists public.messages (
  id uuid primary key default gen_random_uuid(),
  chat_id uuid not null references public.chats(id) on delete cascade,
  sender_id uuid not null references auth.users(id) on delete cascade,
  content text not null,
  created_at timestamptz not null default timezone('utc', now()),
  updated_at timestamptz not null default timezone('utc', now()),
  constraint messages_content_len check (char_length(content) between 1 and 1200)
);

-- =========================
-- Indexes
-- =========================

create index if not exists idx_profiles_username on public.profiles (username);
create index if not exists idx_profiles_banned on public.profiles (banned);

create index if not exists idx_posts_user_id_created_at on public.posts (user_id, created_at desc);
create index if not exists idx_posts_created_at on public.posts (created_at desc);
create index if not exists idx_posts_likes_count on public.posts (likes_count desc);
create index if not exists idx_posts_featured on public.posts (featured) where featured = true;

create index if not exists idx_post_likes_user_id on public.post_likes (user_id);
create index if not exists idx_post_likes_post_id on public.post_likes (post_id);

create index if not exists idx_comments_post_id_created_at on public.comments (post_id, created_at asc);
create index if not exists idx_comments_user_id_created_at on public.comments (user_id, created_at desc);

create index if not exists idx_tracks_user_id_created_at on public.tracks (user_id, created_at desc);
create index if not exists idx_tracks_created_at on public.tracks (created_at desc);
create index if not exists idx_tracks_featured on public.tracks (featured) where featured = true;

create index if not exists idx_chat_members_user_id on public.chat_members (user_id);
create index if not exists idx_messages_chat_id_created_at on public.messages (chat_id, created_at asc);
create index if not exists idx_messages_sender_id_created_at on public.messages (sender_id, created_at desc);

-- =========================
-- Triggers
-- =========================

drop trigger if exists trg_profiles_updated_at on public.profiles;
create trigger trg_profiles_updated_at
before update on public.profiles
for each row execute procedure public.set_updated_at();

drop trigger if exists trg_posts_updated_at on public.posts;
create trigger trg_posts_updated_at
before update on public.posts
for each row execute procedure public.set_updated_at();

drop trigger if exists trg_comments_updated_at on public.comments;
create trigger trg_comments_updated_at
before update on public.comments
for each row execute procedure public.set_updated_at();

drop trigger if exists trg_tracks_updated_at on public.tracks;
create trigger trg_tracks_updated_at
before update on public.tracks
for each row execute procedure public.set_updated_at();

drop trigger if exists trg_chats_updated_at on public.chats;
create trigger trg_chats_updated_at
before update on public.chats
for each row execute procedure public.set_updated_at();

drop trigger if exists trg_messages_updated_at on public.messages;
create trigger trg_messages_updated_at
before update on public.messages
for each row execute procedure public.set_updated_at();

-- Keep posts.likes_count in sync from post_likes
create or replace function public.handle_post_like_delta()
returns trigger
language plpgsql
as $$
begin
  if tg_op = 'INSERT' then
    update public.posts
    set likes_count = likes_count + 1
    where id = new.post_id;
    return new;
  elsif tg_op = 'DELETE' then
    update public.posts
    set likes_count = greatest(0, likes_count - 1)
    where id = old.post_id;
    return old;
  end if;
  return null;
end;
$$;

drop trigger if exists trg_post_like_delta on public.post_likes;
create trigger trg_post_like_delta
after insert or delete on public.post_likes
for each row execute procedure public.handle_post_like_delta();

-- Auto-create profile on new auth user
create or replace function public.handle_new_user_profile()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
declare
  suggested_username text;
begin
  suggested_username :=
    coalesce(new.raw_user_meta_data ->> 'username', split_part(new.email, '@', 1), 'riot_user');
  suggested_username := left(regexp_replace(lower(suggested_username), '[^a-z0-9_]', '', 'g'), 32);
  if char_length(suggested_username) < 3 then
    suggested_username := 'riot_' || substring(new.id::text from 1 for 8);
  end if;

  insert into public.profiles (user_id, username)
  values (new.id, suggested_username)
  on conflict (user_id) do nothing;
  return new;
end;
$$;

drop trigger if exists on_auth_user_created_profile on auth.users;
create trigger on_auth_user_created_profile
after insert on auth.users
for each row execute procedure public.handle_new_user_profile();

-- =========================
-- RLS
-- =========================

alter table public.admin_users enable row level security;
alter table public.profiles enable row level security;
alter table public.posts enable row level security;
alter table public.post_likes enable row level security;
alter table public.comments enable row level security;
alter table public.tracks enable row level security;
alter table public.chats enable row level security;
alter table public.chat_members enable row level security;
alter table public.messages enable row level security;

-- admin_users (self can read own admin row; admins can read all)
drop policy if exists admin_users_select_self_or_admin on public.admin_users;
create policy admin_users_select_self_or_admin
on public.admin_users
for select
using (auth.uid() = user_id or public.is_admin(auth.uid()));

-- profiles
drop policy if exists profiles_public_read on public.profiles;
create policy profiles_public_read
on public.profiles
for select
using (true);

drop policy if exists profiles_insert_self on public.profiles;
create policy profiles_insert_self
on public.profiles
for insert
with check (auth.uid() = user_id);

drop policy if exists profiles_update_self_or_admin on public.profiles;
create policy profiles_update_self_or_admin
on public.profiles
for update
using (auth.uid() = user_id or public.is_admin(auth.uid()))
with check (auth.uid() = user_id or public.is_admin(auth.uid()));

drop policy if exists profiles_delete_admin_only on public.profiles;
create policy profiles_delete_admin_only
on public.profiles
for delete
using (public.is_admin(auth.uid()));

-- posts
drop policy if exists posts_public_read on public.posts;
create policy posts_public_read
on public.posts
for select
using (true);

drop policy if exists posts_insert_self_not_banned on public.posts;
create policy posts_insert_self_not_banned
on public.posts
for insert
with check (
  auth.uid() = user_id
  and not exists (
    select 1 from public.profiles p
    where p.user_id = auth.uid() and p.banned = true
  )
);

drop policy if exists posts_update_owner_or_admin on public.posts;
create policy posts_update_owner_or_admin
on public.posts
for update
using (auth.uid() = user_id or public.is_admin(auth.uid()))
with check (auth.uid() = user_id or public.is_admin(auth.uid()));

drop policy if exists posts_delete_owner_or_admin on public.posts;
create policy posts_delete_owner_or_admin
on public.posts
for delete
using (auth.uid() = user_id or public.is_admin(auth.uid()));

-- post_likes
drop policy if exists post_likes_public_read on public.post_likes;
create policy post_likes_public_read
on public.post_likes
for select
using (true);

drop policy if exists post_likes_insert_self on public.post_likes;
create policy post_likes_insert_self
on public.post_likes
for insert
with check (auth.uid() = user_id);

drop policy if exists post_likes_delete_self_or_admin on public.post_likes;
create policy post_likes_delete_self_or_admin
on public.post_likes
for delete
using (auth.uid() = user_id or public.is_admin(auth.uid()));

-- comments
drop policy if exists comments_public_read on public.comments;
create policy comments_public_read
on public.comments
for select
using (true);

drop policy if exists comments_insert_self_not_banned on public.comments;
create policy comments_insert_self_not_banned
on public.comments
for insert
with check (
  auth.uid() = user_id
  and not exists (
    select 1 from public.profiles p
    where p.user_id = auth.uid() and p.banned = true
  )
);

drop policy if exists comments_update_owner_or_admin on public.comments;
create policy comments_update_owner_or_admin
on public.comments
for update
using (auth.uid() = user_id or public.is_admin(auth.uid()))
with check (auth.uid() = user_id or public.is_admin(auth.uid()));

drop policy if exists comments_delete_owner_or_admin on public.comments;
create policy comments_delete_owner_or_admin
on public.comments
for delete
using (auth.uid() = user_id or public.is_admin(auth.uid()));

-- tracks
drop policy if exists tracks_public_read on public.tracks;
create policy tracks_public_read
on public.tracks
for select
using (true);

drop policy if exists tracks_insert_self_not_banned on public.tracks;
create policy tracks_insert_self_not_banned
on public.tracks
for insert
with check (
  auth.uid() = user_id
  and not exists (
    select 1 from public.profiles p
    where p.user_id = auth.uid() and p.banned = true
  )
);

drop policy if exists tracks_update_owner_or_admin on public.tracks;
create policy tracks_update_owner_or_admin
on public.tracks
for update
using (auth.uid() = user_id or public.is_admin(auth.uid()))
with check (auth.uid() = user_id or public.is_admin(auth.uid()));

drop policy if exists tracks_delete_owner_or_admin on public.tracks;
create policy tracks_delete_owner_or_admin
on public.tracks
for delete
using (auth.uid() = user_id or public.is_admin(auth.uid()));

-- chats
drop policy if exists chats_member_or_admin_read on public.chats;
create policy chats_member_or_admin_read
on public.chats
for select
using (
  public.is_admin(auth.uid())
  or exists (
    select 1 from public.chat_members cm
    where cm.chat_id = id and cm.user_id = auth.uid()
  )
);

drop policy if exists chats_create_self on public.chats;
create policy chats_create_self
on public.chats
for insert
with check (auth.uid() = created_by);

drop policy if exists chats_update_owner_or_admin on public.chats;
create policy chats_update_owner_or_admin
on public.chats
for update
using (auth.uid() = created_by or public.is_admin(auth.uid()))
with check (auth.uid() = created_by or public.is_admin(auth.uid()));

drop policy if exists chats_delete_owner_or_admin on public.chats;
create policy chats_delete_owner_or_admin
on public.chats
for delete
using (auth.uid() = created_by or public.is_admin(auth.uid()));

-- chat_members
drop policy if exists chat_members_member_or_admin_read on public.chat_members;
create policy chat_members_member_or_admin_read
on public.chat_members
for select
using (
  public.is_admin(auth.uid())
  or user_id = auth.uid()
  or exists (
    select 1 from public.chat_members cm
    where cm.chat_id = chat_id and cm.user_id = auth.uid()
  )
);

drop policy if exists chat_members_owner_or_admin_manage on public.chat_members;
create policy chat_members_owner_or_admin_manage
on public.chat_members
for all
using (
  public.is_admin(auth.uid())
  or exists (
    select 1 from public.chat_members cm
    where cm.chat_id = chat_id and cm.user_id = auth.uid() and cm.role in ('owner', 'admin')
  )
)
with check (
  public.is_admin(auth.uid())
  or exists (
    select 1 from public.chat_members cm
    where cm.chat_id = chat_id and cm.user_id = auth.uid() and cm.role in ('owner', 'admin')
  )
);

-- messages
drop policy if exists messages_chat_member_read on public.messages;
create policy messages_chat_member_read
on public.messages
for select
using (
  public.is_admin(auth.uid())
  or exists (
    select 1 from public.chat_members cm
    where cm.chat_id = chat_id and cm.user_id = auth.uid()
  )
);

drop policy if exists messages_chat_member_insert on public.messages;
create policy messages_chat_member_insert
on public.messages
for insert
with check (
  auth.uid() = sender_id
  and exists (
    select 1 from public.chat_members cm
    where cm.chat_id = chat_id and cm.user_id = auth.uid()
  )
);

drop policy if exists messages_sender_or_admin_delete on public.messages;
create policy messages_sender_or_admin_delete
on public.messages
for delete
using (auth.uid() = sender_id or public.is_admin(auth.uid()));

-- =========================
-- Storage buckets + policies
-- =========================

insert into storage.buckets (id, name, public)
values
  ('avatars', 'avatars', true),
  ('post-images', 'post-images', true),
  ('tracks', 'tracks', true)
on conflict (id) do nothing;

-- Read policies (public)
drop policy if exists avatars_public_read on storage.objects;
create policy avatars_public_read
on storage.objects
for select
using (bucket_id = 'avatars');

drop policy if exists post_images_public_read on storage.objects;
create policy post_images_public_read
on storage.objects
for select
using (bucket_id = 'post-images');

drop policy if exists tracks_public_read on storage.objects;
create policy tracks_public_read
on storage.objects
for select
using (bucket_id = 'tracks');

-- Upload policies (owner only)
drop policy if exists avatars_owner_upload on storage.objects;
create policy avatars_owner_upload
on storage.objects
for insert
with check (
  bucket_id = 'avatars'
  and auth.uid() is not null
  and (storage.foldername(name))[1] = 'avatars'
  and (storage.foldername(name))[2] = auth.uid()::text
);

drop policy if exists post_images_owner_upload on storage.objects;
create policy post_images_owner_upload
on storage.objects
for insert
with check (
  bucket_id = 'post-images'
  and auth.uid() is not null
  and (storage.foldername(name))[1] = 'posts'
  and (storage.foldername(name))[2] = auth.uid()::text
);

drop policy if exists tracks_owner_upload on storage.objects;
create policy tracks_owner_upload
on storage.objects
for insert
with check (
  bucket_id = 'tracks'
  and auth.uid() is not null
  and (storage.foldername(name))[1] = 'tracks'
  and (storage.foldername(name))[2] = auth.uid()::text
);

-- Update/Delete object policies
drop policy if exists storage_owner_or_admin_update on storage.objects;
create policy storage_owner_or_admin_update
on storage.objects
for update
using (
  auth.uid() is not null
  and (
    ((storage.foldername(name))[2] = auth.uid()::text and bucket_id in ('avatars', 'post-images', 'tracks'))
    or public.is_admin(auth.uid())
  )
)
with check (
  auth.uid() is not null
  and (
    ((storage.foldername(name))[2] = auth.uid()::text and bucket_id in ('avatars', 'post-images', 'tracks'))
    or public.is_admin(auth.uid())
  )
);

drop policy if exists storage_owner_or_admin_delete on storage.objects;
create policy storage_owner_or_admin_delete
on storage.objects
for delete
using (
  auth.uid() is not null
  and (
    ((storage.foldername(name))[2] = auth.uid()::text and bucket_id in ('avatars', 'post-images', 'tracks'))
    or public.is_admin(auth.uid())
  )
);

commit;
