-- Le Tribunal — schéma V2 + V3 (auth par email, rôles, verrou de pseudo,
-- points/journal/podium/indicateurs de progression)
-- À exécuter dans l'éditeur SQL du projet Supabase.

-- ============================================================
-- Migration depuis V1 (authentification pseudo + email interne)
-- ============================================================
-- V1 utilisait un email fictif dérivé du pseudo (slug@letribunal.test).
-- V2 exige un vrai email par utilisateur (unique, confirmé). Les comptes
-- créés sous V1 sont donc invalides pour V2. Comme il ne s'agit que de
-- données de test, on repart de zéro :
--
--   1. Dashboard Supabase → Authentication → Providers → Email
--      → activer "Confirm email".
--   2. SQL Editor → exécuter la ligne ci-dessous pour supprimer tous les
--      comptes de test (cascade automatiquement sur public.profiles) :
--
--        delete from auth.users;
--
--   3. Exécuter tout le script ci-dessous.
--   4. Storage → bucket "avatars" → vider les fichiers de test existants
--      (les anciens noms de fichiers restent valides, ils ne gênent pas,
--      mais autant repartir propre).
-- ============================================================

-- 1. Table profiles ----------------------------------------------------

create table if not exists public.profiles (
  id uuid primary key references auth.users (id) on delete cascade,
  pseudo text not null,
  avatar_url text,
  points integer not null default 0,
  role text not null default 'public' check (role in ('public', 'judge')),
  pseudo_locked boolean not null default false,
  previous_rank integer,
  created_at timestamptz not null default now()
);

-- Colonnes ajoutées si la table existait déjà depuis une version antérieure
-- (no-op si déjà présentes).
alter table public.profiles add column if not exists role text not null default 'public';
alter table public.profiles add column if not exists pseudo_locked boolean not null default false;
alter table public.profiles add column if not exists previous_rank integer;
alter table public.profiles drop column if exists slug;

alter table public.profiles drop constraint if exists profiles_role_check;
alter table public.profiles add constraint profiles_role_check check (role in ('public', 'judge'));

-- Unicité du pseudo affiché (insensible à la casse).
drop index if exists profiles_slug_key;
create unique index if not exists profiles_pseudo_lower_key on public.profiles (lower(pseudo));

alter table public.profiles enable row level security;

-- Pas de grant insert : la création du profil passe exclusivement par le
-- trigger handle_new_user() (SECURITY DEFINER, contourne la RLS).
grant select, update on public.profiles to authenticated;

-- 2. Fonctions & triggers -------------------------------------------------
-- La RLS est au niveau ligne : elle ne peut pas exprimer "cette colonne
-- seulement si tel rôle". On verrouille donc les colonnes sensibles
-- (role, points, pseudo_locked, pseudo figé) via des triggers, qui ne
-- font jamais confiance à ce que le client envoie.

-- À la création d'un profil : on ignore ce que le client a pu envoyer
-- pour role/pseudo_locked/points et on force les valeurs par défaut.
-- Le pseudo initial est repris depuis auth.users si non fourni explicitement.
create or replace function public.enforce_profile_insert()
returns trigger
language plpgsql
as $$
begin
  new.role := 'public';
  new.pseudo_locked := false;
  new.points := 0;
  return new;
end;
$$;

drop trigger if exists profiles_before_insert on public.profiles;
create trigger profiles_before_insert
  before insert on public.profiles
  for each row execute function public.enforce_profile_insert();

-- À la mise à jour : selon le rôle de l'appelant (auth.uid()), on
-- autorise ou rejette les changements de colonnes sensibles.
create or replace function public.enforce_profile_update()
returns trigger
language plpgsql
as $$
declare
  caller_role text;
  points_bypass boolean := coalesce(current_setting('app.bypass_points_lock', true), 'off') = 'on';
begin
  -- auth.uid() est NULL hors contexte d'une requête utilisateur (SQL Editor,
  -- migrations, clé service_role). Ces accès sont déjà pleinement fiables
  -- par construction (accès direct à la base), donc on les laisse passer.
  if auth.uid() is null then
    return new;
  end if;

  -- `points` ne peut être modifié que via la RPC award_points(), jamais
  -- directement par le client — même un juge ne peut pas l'écrire à la main.
  if new.points is distinct from old.points and not points_bypass then
    raise exception 'points must be modified via award_points()';
  end if;

  select role into caller_role from public.profiles where id = auth.uid();

  if caller_role = 'judge' then
    -- Un juge peut changer pseudo / pseudo_locked / role / avatar_url /
    -- previous_rank de n'importe qui.
    return new;
  end if;

  -- Appelant non-juge : ne peut modifier que sa propre ligne (déjà imposé
  -- par la RLS), et seulement pseudo / avatar_url, sous conditions.
  if new.role is distinct from old.role then
    raise exception 'only judges can change role';
  end if;

  if new.previous_rank is distinct from old.previous_rank then
    raise exception 'only judges can reset the rank reference';
  end if;

  if new.pseudo_locked is distinct from old.pseudo_locked and new.pseudo_locked = false then
    raise exception 'only judges can unlock a pseudo';
  end if;

  if new.pseudo is distinct from old.pseudo then
    if old.pseudo_locked then
      raise exception 'pseudo is locked, ask a judge';
    end if;
    -- Premier changement de pseudo : on le fige automatiquement.
    new.pseudo_locked := true;
  end if;

  return new;
end;
$$;

drop trigger if exists profiles_before_update on public.profiles;
create trigger profiles_before_update
  before update on public.profiles
  for each row execute function public.enforce_profile_update();

-- Création automatique du profil à l'inscription (auth.users → profiles),
-- indépendamment de l'état de la session (fonctionne même si l'email
-- n'est pas encore confirmé, contrairement à un insert fait depuis le client).
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = public
as $$
begin
  insert into public.profiles (id, pseudo)
  values (new.id, coalesce(new.raw_user_meta_data ->> 'pseudo', 'Nouveau membre'));
  return new;
end;
$$;

drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();

-- 3. Policies RLS ----------------------------------------------------------

drop policy if exists "Profiles are viewable by authenticated users" on public.profiles;
create policy "Profiles are viewable by authenticated users"
  on public.profiles for select
  to authenticated
  using (true);

-- Plus de policy INSERT côté client : la création du profil passe
-- exclusivement par le trigger handle_new_user() (SECURITY DEFINER).
drop policy if exists "Users can insert their own profile" on public.profiles;

drop policy if exists "Users can update their own profile" on public.profiles;
create policy "Users can update their own profile"
  on public.profiles for update
  to authenticated
  using (auth.uid() = id)
  with check (auth.uid() = id);

drop policy if exists "Judges can update any profile" on public.profiles;
create policy "Judges can update any profile"
  on public.profiles for update
  to authenticated
  using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'))
  with check (true);

-- Aucune policy delete => suppression interdite en V2.

-- 4. Bucket avatars -------------------------------------------------------

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

-- Le bucket public sert la lecture via l'URL publique (hors RLS), mais une
-- policy SELECT reste nécessaire : pour un upload en upsert (remplacement
-- d'une photo existante), Postgres doit pouvoir lire la ligne existante en
-- interne pour évaluer la clause USING de l'UPDATE — sans SELECT, l'upsert
-- échoue avec "new row violates row-level security policy" même si les
-- policies INSERT/UPDATE sont correctes.
drop policy if exists "Avatars are publicly viewable" on storage.objects;
create policy "Avatars are publicly viewable"
  on storage.objects for select
  using (bucket_id = 'avatars');

drop policy if exists "Users can upload their own avatar" on storage.objects;
create policy "Users can upload their own avatar"
  on storage.objects for insert
  to authenticated
  with check (
    bucket_id = 'avatars'
    and split_part(name, '.', 1) = auth.uid()::text
  );

drop policy if exists "Users can update their own avatar" on storage.objects;
create policy "Users can update their own avatar"
  on storage.objects for update
  to authenticated
  using (
    bucket_id = 'avatars'
    and split_part(name, '.', 1) = auth.uid()::text
  )
  with check (
    bucket_id = 'avatars'
    and split_part(name, '.', 1) = auth.uid()::text
  );

-- 5. Vérification du pseudo avant inscription -----------------------------
-- Un visiteur non connecté (rôle anon) n'a pas le droit de lire `profiles`
-- (RLS réservée aux authentifiés). Pour afficher "ce pseudo est déjà pris"
-- avant même de créer le compte, on expose une RPC qui ne renvoie qu'un
-- booléen, sans jamais exposer le contenu de la table aux anonymes.
create or replace function public.is_pseudo_taken(p_pseudo text)
returns boolean
language sql
security definer
set search_path = public
as $$
  select exists (select 1 from public.profiles where lower(pseudo) = lower(p_pseudo));
$$;

grant execute on function public.is_pseudo_taken(text) to anon, authenticated;

-- 6. Points, journal ("le crieur") ------------------------------------------

create table if not exists public.points_log (
  id bigint generated always as identity primary key,
  target_id uuid not null references public.profiles (id) on delete cascade,
  judge_id uuid not null references public.profiles (id) on delete cascade,
  delta integer not null,
  reason text,
  created_at timestamptz not null default now()
);

alter table public.points_log enable row level security;

-- Lecture ouverte à tous les authentifiés (transparence du journal).
-- Aucune policy insert/update/delete : seule la RPC award_points()
-- (SECURITY DEFINER, propriétaire de la table) peut écrire.
revoke insert, update, delete on public.points_log from authenticated, anon;
grant select on public.points_log to authenticated;

drop policy if exists "points_log readable by authenticated" on public.points_log;
create policy "points_log readable by authenticated"
  on public.points_log for select
  to authenticated
  using (true);

-- Seul point d'entrée pour modifier les points : vérifie le rôle juge
-- côté serveur, applique le delta (négatif autorisé, pas de plancher),
-- et trace l'opération dans points_log. Le flag app.bypass_points_lock
-- autorise le trigger enforce_profile_update() à laisser passer CETTE
-- écriture précise sur la colonne points.
create or replace function public.award_points(p_target_id uuid, p_delta int, p_reason text default null)
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
  caller_role text;
begin
  select role into caller_role from public.profiles where id = auth.uid();
  if caller_role is distinct from 'judge' then
    raise exception 'only judges can award points';
  end if;

  if not exists (select 1 from public.profiles where id = p_target_id) then
    raise exception 'target member not found';
  end if;

  perform set_config('app.bypass_points_lock', 'on', true);
  update public.profiles set points = points + p_delta where id = p_target_id;
  perform set_config('app.bypass_points_lock', 'off', true);

  insert into public.points_log (target_id, judge_id, delta, reason)
  values (p_target_id, auth.uid(), p_delta, p_reason);
end;
$$;

grant execute on function public.award_points(uuid, int, text) to authenticated;

-- 7. Repère de classement (flèches de progression) --------------------------
-- Fige le rang actuel de chacun dans previous_rank ; les flèches côté
-- client comparent le rang courant à cette référence.

create or replace function public.reset_rank_reference()
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
  caller_role text;
begin
  select role into caller_role from public.profiles where id = auth.uid();
  if caller_role is distinct from 'judge' then
    raise exception 'only judges can reset the rank reference';
  end if;

  with ranked as (
    select id, rank() over (order by points desc) as r
    from public.profiles
  )
  update public.profiles p
  set previous_rank = ranked.r
  from ranked
  where p.id = ranked.id;
end;
$$;

grant execute on function public.reset_rank_reference() to authenticated;

-- 8. Realtime ----------------------------------------------------------------
-- Permet au leaderboard et au journal de s'actualiser en direct pour tout
-- le monde (Dashboard → Database → Replication fait la même chose).

do $$
begin
  alter publication supabase_realtime add table public.profiles;
exception
  when duplicate_object then null;
end $$;

do $$
begin
  alter publication supabase_realtime add table public.points_log;
exception
  when duplicate_object then null;
end $$;

-- 9. Le Calendrier des Dieux --------------------------------------------------
-- Agenda de la semaine : chaque journée est placée sous le patronage d'une
-- divinité (nom/domaine en texte libre, l'Archonte choisit un préréglage
-- côté client ou saisit le sien). Lecture ouverte à tous, écriture réservée
-- aux juges (Archontes), vérifiée côté serveur par RLS — jamais par le
-- masquage des boutons en front.

create table if not exists public.days (
  id uuid primary key default gen_random_uuid(),
  date date not null unique,
  god_name text not null,
  god_domain text not null,
  description text,
  created_at timestamptz not null default now()
);

create table if not exists public.events (
  id uuid primary key default gen_random_uuid(),
  day_id uuid not null references public.days (id) on delete cascade,
  title text not null,
  description text,
  type text not null default 'activite' check (type in ('activite', 'defi', 'epreuve', 'tribunal')),
  start_time time,
  location text,
  created_by uuid references public.profiles (id) on delete set null,
  created_at timestamptz not null default now()
);

-- Ligne de configuration unique (id toujours = true) : date du Tribunal.
create table if not exists public.settings (
  id boolean primary key default true,
  tribunal_date timestamptz,
  constraint settings_singleton check (id)
);
insert into public.settings (id) values (true) on conflict (id) do nothing;

alter table public.days enable row level security;
alter table public.events enable row level security;
alter table public.settings enable row level security;

grant select, insert, update, delete on public.days, public.events to authenticated;
grant select, update on public.settings to authenticated;

-- Lecture : tout utilisateur authentifié.
drop policy if exists "days viewable by authenticated" on public.days;
create policy "days viewable by authenticated" on public.days for select to authenticated using (true);

drop policy if exists "events viewable by authenticated" on public.events;
create policy "events viewable by authenticated" on public.events for select to authenticated using (true);

drop policy if exists "settings viewable by authenticated" on public.settings;
create policy "settings viewable by authenticated" on public.settings for select to authenticated using (true);

-- Écriture : réservée aux juges.
drop policy if exists "days insert by judges" on public.days;
create policy "days insert by judges" on public.days for insert to authenticated
  with check (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

drop policy if exists "days update by judges" on public.days;
create policy "days update by judges" on public.days for update to authenticated
  using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'))
  with check (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

drop policy if exists "days delete by judges" on public.days;
create policy "days delete by judges" on public.days for delete to authenticated
  using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

drop policy if exists "events insert by judges" on public.events;
create policy "events insert by judges" on public.events for insert to authenticated
  with check (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

drop policy if exists "events update by judges" on public.events;
create policy "events update by judges" on public.events for update to authenticated
  using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'))
  with check (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

drop policy if exists "events delete by judges" on public.events;
create policy "events delete by judges" on public.events for delete to authenticated
  using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

drop policy if exists "settings update by judges" on public.settings;
create policy "settings update by judges" on public.settings for update to authenticated
  using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'))
  with check (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

-- created_by ne fait jamais confiance au client : toujours l'auteur réel.
create or replace function public.enforce_event_insert()
returns trigger
language plpgsql
as $$
begin
  new.created_by := auth.uid();
  return new;
end;
$$;

drop trigger if exists events_before_insert on public.events;
create trigger events_before_insert
  before insert on public.events
  for each row execute function public.enforce_event_insert();

do $$
begin
  alter publication supabase_realtime add table public.days;
exception
  when duplicate_object then null;
end $$;

do $$
begin
  alter publication supabase_realtime add table public.events;
exception
  when duplicate_object then null;
end $$;

do $$
begin
  alter publication supabase_realtime add table public.settings;
exception
  when duplicate_object then null;
end $$;

-- 10. Désigner les premiers juges ---------------------------------------------
-- À faire une fois les comptes créés (SQL Editor) :
--
--   update public.profiles set role = 'judge' where id = '<uuid-du-membre>';
--
-- (Récupérer l'uuid via Authentication → Users, ou :
--   select id, pseudo from public.profiles;)
