-- 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).
-- Le pseudo temporaire inclut les 8 premiers caractères de l'uuid : un
-- littéral fixe "Nouveau membre" pour tout le monde entrait en collision
-- avec profiles_pseudo_lower_key (unique) dès qu'une 2e personne était
-- invitée avant que la 1re ait fini son inscription — ce qui n'était
-- jamais arrivé avant d'inviter plusieurs personnes d'affilée.
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 ' || substr(new.id::text, 1, 8))
  );
  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;)

-- 11. Le Vol d'Icare -----------------------------------------------------------
-- Mini-jeu : remplace la Course du Char (abandonnée — voir nettoyage
-- ci-dessous). Flappy Bird grec : Icare vole entre des colonnes de temple,
-- un seul record personnel all-time (pas de piste/jour, générée librement à
-- chaque partie côté client). Les scores sont figés dès que la date du
-- Tribunal (settings.tribunal_date) est atteinte, puis les gloires du top 3
-- sont attribuées automatiquement (pas d'Archonte impliqué) par une tâche
-- planifiée pg_cron.

-- Nettoyage de la Course du Char (schéma déjà appliqué en prod avant cet
-- abandon — ces DROP sont nécessaires, pas juste cosmétiques).
do $$
begin
  perform cron.unschedule('close-daily-chariot-race');
exception
  when others then null;
end $$;
drop function if exists public.close_daily_chariot_race();
drop table if exists public.chariot_race_closes;
drop function if exists public.submit_chariot_run(integer, jsonb);
drop table if exists public.chariot_runs;

-- Un point attribué automatiquement n'a pas de juge : judge_id reste
-- nullable (déjà appliqué précédemment — le Crieur affiche "Le Tribunal").

create table if not exists public.icarus_scores (
  user_id uuid primary key references public.profiles (id) on delete cascade,
  best_score integer not null check (best_score between 0 and 1000000),
  updated_at timestamptz not null default now()
);

alter table public.icarus_scores enable row level security;

-- Même modèle que points_log/chariot_runs : verrouillée en écriture, seule
-- la RPC submit_icarus_score() (SECURITY DEFINER) peut écrire.
revoke insert, update, delete on public.icarus_scores from authenticated, anon;
grant select on public.icarus_scores to authenticated;

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

-- Marqueur d'idempotence pour l'attribution automatique des gloires (un seul
-- événement, pas de notion de jour comme pour la Course du Char).
alter table public.settings add column if not exists icarus_points_awarded boolean not null default false;

-- Seul point d'entrée pour soumettre un score. Une fois la date du Tribunal
-- atteinte, les scores sont figés : la RPC ne fait plus rien (retourne le
-- record existant sans le modifier) plutôt que d'échouer bruyamment.
create or replace function public.submit_icarus_score(p_score integer)
returns public.icarus_scores
language plpgsql
security definer
set search_path = public
as $$
declare
  v_tribunal_date timestamptz;
  v_row public.icarus_scores;
begin
  if auth.uid() is null then
    raise exception 'authentication required';
  end if;

  if p_score is null or p_score < 0 or p_score > 1000000 then
    raise exception 'invalid score';
  end if;

  select tribunal_date into v_tribunal_date from public.settings where id = true;

  if v_tribunal_date is not null and now() >= v_tribunal_date then
    select * into v_row from public.icarus_scores where user_id = auth.uid();
    return v_row;
  end if;

  insert into public.icarus_scores (user_id, best_score, updated_at)
  values (auth.uid(), p_score, now())
  on conflict (user_id) do update
    set best_score = excluded.best_score,
        updated_at = now()
    where excluded.best_score > public.icarus_scores.best_score;

  select * into v_row from public.icarus_scores where user_id = auth.uid();
  return v_row;
end;
$$;

grant execute on function public.submit_icarus_score(integer) to authenticated;

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

-- Attribue les gloires du top 3 (ex-aequo inclus au même rang) dès que la
-- date du Tribunal est atteinte ; no-op tant qu'elle n'est pas encore
-- passée, et no-op définitif une fois déjà fait (icarus_points_awarded).
-- Volontairement pas de grant execute à authenticated : uniquement appelée
-- par pg_cron ou depuis le SQL Editor.
create or replace function public.award_icarus_points_if_due()
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
  v_tribunal_date timestamptz;
  v_already_awarded boolean;
  r record;
  v_points int;
begin
  select tribunal_date, icarus_points_awarded into v_tribunal_date, v_already_awarded
  from public.settings where id = true;

  if v_tribunal_date is null or now() < v_tribunal_date or v_already_awarded then
    return;
  end if;

  for r in
    with ranked as (
      select user_id, best_score,
             rank() over (order by best_score desc) as rnk
      from public.icarus_scores
    )
    select * from ranked where rnk <= 3
  loop
    v_points := case r.rnk when 1 then 3 when 2 then 2 when 3 then 1 else 0 end;
    update public.profiles set points = points + v_points where id = r.user_id;
    insert into public.points_log (target_id, judge_id, delta, reason)
    values (r.user_id, null, v_points, 'Le Vol d''Icare — rang ' || r.rnk || ' au Tribunal');
  end loop;

  update public.settings set icarus_points_awarded = true where id = true;
end;
$$;

-- ⚠️ pg_cron doit être activé une fois pour toutes via le Dashboard Supabase
-- (Database → Extensions → "pg_cron" → Enable) — pas scriptable depuis ce
-- fichier, et pas garanti self-service selon le plan/la région du projet.
-- Les deux blocs ci-dessous n'échouent jamais bruyamment si l'extension n'est
-- pas encore activée (schéma "cron" inexistant) : le reste de ce script,
-- ré-exécuté en entier à chaque changement, doit toujours pouvoir passer.
do $$
begin
  perform cron.unschedule('award-icarus-points');
exception
  when others then null; -- la tâche n'existe pas encore, ou pg_cron pas activé
end $$;

do $$
begin
  perform cron.schedule(
    'award-icarus-points',
    '*/15 * * * *',
    $cron$select public.award_icarus_points_if_due();$cron$
  );
exception
  when others then
    raise notice 'pg_cron indisponible : active l''extension via le Dashboard Supabase (Database → Extensions → pg_cron), puis ré-exécute ce script pour planifier l''attribution automatique des gloires du Vol d''Icare.';
end $$;

-- 12. Le Jeu de l'Agora — Le Char et Le Gardien du Silence ---------------------
-- Outils pour la soirée du Tribunal elle-même (jeu physique/social, pas un
-- mini-jeu numérique) : l'app affiche/synchronise un état que les Archontes
-- pilotent à la main, elle ne calcule/arbitre jamais le déroulé du jeu.
-- Préfixe chariot_* (pas char_*) : sans rapport avec l'ancienne Course du
-- Char digitale déjà supprimée (section 11), qui utilisait déjà ce préfixe —
-- le reprendre ici évite de laisser croire à une résurrection de ce mini-jeu.
-- La route reste /char.

create table if not exists public.chariot_questions (
  id uuid primary key default gen_random_uuid(),
  text text not null,
  position integer not null default 0,
  created_at timestamptz not null default now()
);

alter table public.chariot_questions enable row level security;
grant select, insert, update, delete on public.chariot_questions to authenticated;

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

drop policy if exists "chariot_questions insert by judges" on public.chariot_questions;
create policy "chariot_questions insert by judges" on public.chariot_questions 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 "chariot_questions update by judges" on public.chariot_questions;
create policy "chariot_questions update by judges" on public.chariot_questions 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 "chariot_questions delete by judges" on public.chariot_questions;
create policy "chariot_questions delete by judges" on public.chariot_questions for delete to authenticated
  using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));

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

-- Nettoyage de l'ancien modèle par statut (team_a/team_b/out), remplacé par
-- un modèle par emplacement fixe ci-dessous — schéma jamais appliqué en
-- prod (retour utilisateur avant la première vraie migration), DROP direct.
drop table if exists public.chariot_roster;

-- Char : 6 emplacements fixes (1-3 = Colonne A, 4-6 = Colonne B), vides
-- (user_id null) ou occupés. Pré-remplis une fois pour toutes ci-dessous —
-- on ne fait jamais que les UPDATE, jamais d'insert/delete côté client.
create table if not exists public.chariot_slots (
  slot integer primary key check (slot between 1 and 6),
  user_id uuid references public.profiles (id) on delete set null
);
insert into public.chariot_slots (slot)
select generate_series(1, 6)
on conflict (slot) do nothing;

alter table public.chariot_slots enable row level security;
grant select, update on public.chariot_slots to authenticated;

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

drop policy if exists "chariot_slots update by judges" on public.chariot_slots;
create policy "chariot_slots update by judges" on public.chariot_slots 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'));

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

-- Journal d'entrées dans le char (append-only, comme points_log) : sert
-- uniquement à compter combien de fois chaque personne est montée —
-- une ligne ajoutée à chaque affectation d'un emplacement, jamais modifiée.
create table if not exists public.chariot_entries (
  id bigint generated always as identity primary key,
  user_id uuid not null references public.profiles (id) on delete cascade,
  created_at timestamptz not null default now()
);

alter table public.chariot_entries enable row level security;
grant select, insert on public.chariot_entries to authenticated;
revoke update, delete on public.chariot_entries from authenticated, anon;

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

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

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

-- Question actuellement révélée sur la page publique (bascule afficher/
-- masquer), et état du Gardien du Silence.
alter table public.settings add column if not exists chariot_revealed_question_id uuid references public.chariot_questions (id) on delete set null;
alter table public.settings add column if not exists gardien_holder_id uuid references public.profiles (id) on delete set null;
alter table public.settings add column if not exists gardien_expires_at timestamptz;

-- Verrou de colonne pour le Gardien (même pattern que profiles.points) :
-- gardien_holder_id/gardien_expires_at ne doivent changer que via un vrai
-- tirage aléatoire (reroll_gardien), jamais par une écriture directe même
-- par un Archonte — chariot_revealed_question_id reste, lui, directement
-- modifiable par les juges (rien à protéger dans cette colonne-là).
create or replace function public.enforce_settings_update()
returns trigger
language plpgsql
as $$
declare
  bypass boolean := coalesce(current_setting('app.bypass_gardien_lock', true), 'off') = 'on';
begin
  if auth.uid() is null then
    return new;
  end if;

  if not bypass and (
    new.gardien_holder_id is distinct from old.gardien_holder_id
    or new.gardien_expires_at is distinct from old.gardien_expires_at
  ) then
    raise exception 'gardien_holder_id/gardien_expires_at can only change via reroll_gardien()';
  end if;

  return new;
end;
$$;

drop trigger if exists settings_before_update on public.settings;
create trigger settings_before_update
  before update on public.settings
  for each row execute function public.enforce_settings_update();

-- Tire un nouveau Gardien (uniquement parmi les Citoyens, jamais le
-- détenteur actuel si possible) et repousse l'expiration de 5 minutes.
-- p_force=true (réservé aux juges) reroll immédiatement ; p_force=false
-- (utilisé par le minuteur côté client, et par pg_cron en filet) ne fait
-- rien tant que gardien_expires_at n'est pas atteint.
--
-- Un seul UPDATE ... WHERE atomique (pas de SELECT puis UPDATE séparés) :
-- avec plusieurs téléphones ouverts, tous les minuteurs locaux arrivent à
-- zéro dans la même seconde et appellent cette RPC en même temps — la garde
-- dans le WHERE fait que Postgres sérialise naturellement, un seul appel
-- modifie réellement la ligne, les autres ne touchent aucune ligne.
create or replace function public.reroll_gardien(p_force boolean default false)
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
  caller_role text;
begin
  if p_force then
    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 force a reroll';
    end if;
  end if;

  perform set_config('app.bypass_gardien_lock', 'on', true);

  update public.settings
  set gardien_holder_id = coalesce(
        (select id from public.profiles
         where role = 'public' and id is distinct from settings.gardien_holder_id
         order by random() limit 1),
        (select id from public.profiles where role = 'public' order by random() limit 1)
      ),
      gardien_expires_at = now() + interval '5 minutes'
  where id = true
    and (p_force or gardien_expires_at is null or now() >= gardien_expires_at);

  perform set_config('app.bypass_gardien_lock', 'off', true);
end;
$$;

grant execute on function public.reroll_gardien(boolean) to authenticated;

-- ⚠️ Même remarque que pour pg_cron plus haut : à activer une fois via le
-- Dashboard Supabase si ce n'est pas déjà fait. Cadence 1 minute : filet
-- large par rapport au minuteur de 5 minutes, le déclenchement réel se fait
-- côté client dès qu'un onglet est ouvert.
do $$
begin
  perform cron.unschedule('reroll-gardien');
exception
  when others then null;
end $$;

do $$
begin
  perform cron.schedule(
    'reroll-gardien',
    '*/1 * * * *',
    $cron$select public.reroll_gardien();$cron$
  );
exception
  when others then
    raise notice 'pg_cron indisponible : active l''extension via le Dashboard Supabase (Database → Extensions → pg_cron), puis ré-exécute ce script pour planifier le reroll automatique du Gardien.';
end $$;

-- 13. Suggestions de questions par les Citoyens + emails sur Admin
-- -----------------------------------------------------------------

-- Une proposition par Citoyen (une ligne par user_id, comme icarus_scores).
-- Verrouillée en écriture comme points_log/icarus_scores : seule la RPC
-- submit_chariot_question() (SECURITY DEFINER) peut insert/update ; les
-- juges gardent un accès direct en lecture (modération) et en suppression
-- (rejet), sans passer par une RPC pour ce cas simple.
create table if not exists public.chariot_submissions (
  user_id uuid primary key references public.profiles (id) on delete cascade,
  text text not null,
  updated_at timestamptz not null default now()
);

alter table public.chariot_submissions enable row level security;
revoke insert, update on public.chariot_submissions from authenticated, anon;
grant select, delete on public.chariot_submissions to authenticated;

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

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

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

-- Historique append-only (comme points_log) des versions successives d'une
-- proposition : chariot_submissions ne garde que la valeur courante (upsert),
-- donc sans ce journal les Archontes ne verraient jamais les versions
-- précédentes d'une question modifiée plusieurs fois. Écrit uniquement par
-- submit_chariot_question() (aucune policy insert, comme points_log).
create table if not exists public.chariot_submission_history (
  id bigint generated always as identity primary key,
  user_id uuid not null references public.profiles (id) on delete cascade,
  text text not null,
  created_at timestamptz not null default now()
);

alter table public.chariot_submission_history enable row level security;
revoke insert, update, delete on public.chariot_submission_history from authenticated, anon;
grant select on public.chariot_submission_history to authenticated;

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

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

-- Seul point d'entrée pour proposer/modifier sa question. Réutilise
-- settings.tribunal_date comme déclencheur de gel — même philosophie que
-- submit_icarus_score() : une fois l'Agora commencée, no-op silencieux
-- (retourne la ligne existante sans la modifier) plutôt qu'une erreur.
create or replace function public.submit_chariot_question(p_text text)
returns public.chariot_submissions
language plpgsql
security definer
set search_path = public
as $$
declare
  v_caller_role text;
  v_tribunal_date timestamptz;
  v_row public.chariot_submissions;
begin
  if auth.uid() is null then
    raise exception 'authentication required';
  end if;

  select role into v_caller_role from public.profiles where id = auth.uid();
  if v_caller_role = 'judge' then
    raise exception 'only citizens can submit a chariot question';
  end if;

  if p_text is null or length(trim(p_text)) = 0 then
    raise exception 'question vide';
  end if;
  if length(p_text) > 300 then
    raise exception 'question trop longue';
  end if;

  select tribunal_date into v_tribunal_date from public.settings where id = true;

  if v_tribunal_date is not null and now() >= v_tribunal_date then
    select * into v_row from public.chariot_submissions where user_id = auth.uid();
    return v_row;
  end if;

  insert into public.chariot_submissions (user_id, text, updated_at)
  values (auth.uid(), trim(p_text), now())
  on conflict (user_id) do update
    set text = excluded.text,
        updated_at = now();

  insert into public.chariot_submission_history (user_id, text)
  values (auth.uid(), trim(p_text));

  select * into v_row from public.chariot_submissions where user_id = auth.uid();
  return v_row;
end;
$$;

grant execute on function public.submit_chariot_question(text) to authenticated;

-- Émails des membres pour /admin : jamais stockés dans profiles (voir §4 du
-- doc — le pseudo est le seul nom montré aux autres), donc invisibles côté
-- client sans passer par auth.users. Plutôt que d'exposer la service_role
-- key côté client, une RPC SECURITY DEFINER fait la jointure, gardée par la
-- même vérification de rôle que les autres RPC juge-only du projet.
create or replace function public.admin_list_members()
returns table (
  id uuid,
  pseudo text,
  avatar_url text,
  role text,
  pseudo_locked boolean,
  email text
)
language plpgsql
security definer
set search_path = public
as $$
begin
  -- Colonnes de sortie qualifiées par un alias partout : returns table(...)
  -- déclare id/role comme variables PL/pgSQL, qui masqueraient sinon les
  -- colonnes de même nom dans profiles (erreur 42702 "ambiguous").
  if not exists (select 1 from public.profiles pr where pr.id = auth.uid() and pr.role = 'judge') then
    raise exception 'only judges can list members';
  end if;

  return query
    select p.id, p.pseudo, p.avatar_url, p.role, p.pseudo_locked, u.email::text
    from public.profiles p
    join auth.users u on u.id = p.id
    order by p.pseudo asc;
end;
$$;

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

-- 14. Le Mur de la Honte -------------------------------------------------------
-- Une note anonyme par Citoyen par jour, max 10 notes visibles par jour
-- (remise à zéro chaque jour — filtrage par date, pas de suppression), pour
-- donner une raison de rouvrir l'app tous les jours de la semaine, pas
-- seulement pour le classement. L'anonymat n'est qu'à moitié réel : les
-- Archontes voient l'historique complet avec l'auteur de chaque note.
-- Chaque note peut porter une image facultative (bucket wall-images,
-- compressée côté client, limite de taille côté serveur) et des réactions
-- emoji, elles aussi anonymes (voir wall_note_reactions plus bas).

create table if not exists public.wall_notes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references public.profiles (id) on delete cascade,
  text text not null,
  created_at timestamptz not null default now()
);

-- URL publique dans le bucket wall-images (voir plus bas), facultative.
alter table public.wall_notes add column if not exists image_url text;

alter table public.wall_notes enable row level security;
revoke insert, update, delete on public.wall_notes from authenticated, anon;
grant select on public.wall_notes to authenticated;

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

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

-- Seul moyen pour un Citoyen de voir les notes des autres sans jamais
-- exposer qui les a écrites côté client : la RLS ne peut pas masquer une
-- colonne pour certaines lignes, donc cette RPC (SECURITY DEFINER) fait le
-- filtre elle-même et ne renvoie jamais user_id. Le type de retour change
-- (ajout d'image_url) : drop explicite requis, create or replace ne peut
-- pas changer le type de retour d'une fonction existante.
drop function if exists public.wall_notes_today();

create or replace function public.wall_notes_today()
returns table (id uuid, text text, image_url text, created_at timestamptz)
language sql
security definer
set search_path = public
as $$
  select w.id, w.text, w.image_url, w.created_at
  from public.wall_notes w
  where (w.created_at at time zone 'Europe/Paris')::date = (now() at time zone 'Europe/Paris')::date
  order by w.created_at asc;
$$;

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

-- Seul point d'entrée pour publier une note. Pas de gel via tribunal_date
-- (contrairement au Char) : contrairement à la soirée de clôture, c'est une
-- mécanique de toute la semaine. Petite fenêtre de course possible si deux
-- personnes postent la même seconde alors qu'il reste 1 place (double
-- insertion à 11) — acceptée sciemment, comme d'autres arbitrages similaires
-- déjà faits dans le projet pour un groupe de ~12 amis. La signature change
-- (ajout de p_image_url) : l'ancienne (text) est explicitement supprimée,
-- sinon create or replace créerait une 2e surcharge au lieu de remplacer.
drop function if exists public.post_wall_note(text);

create or replace function public.post_wall_note(p_text text, p_image_url text default null)
returns public.wall_notes
language plpgsql
security definer
set search_path = public
as $$
declare
  v_caller_role text;
  v_today date := (now() at time zone 'Europe/Paris')::date;
  v_already_posted boolean;
  v_count_today int;
  v_image_url text := nullif(trim(coalesce(p_image_url, '')), '');
  v_row public.wall_notes;
begin
  if auth.uid() is null then
    raise exception 'authentication required';
  end if;

  select role into v_caller_role from public.profiles where id = auth.uid();
  if v_caller_role = 'judge' then
    raise exception 'only citizens can post on the wall';
  end if;

  if p_text is null or length(trim(p_text)) = 0 then
    raise exception 'note vide';
  end if;
  if length(p_text) > 200 then
    raise exception 'note trop longue';
  end if;

  -- Doit venir du bucket wall-images géré par l'app, jamais une URL
  -- arbitraire (le bucket a lui-même une limite de taille/mime, voir plus
  -- bas — cette vérification empêche seulement d'y stocker n'importe quoi).
  if v_image_url is not null and v_image_url not like '%/wall-images/%' then
    raise exception 'image invalide';
  end if;

  select exists (
    select 1 from public.wall_notes
    where user_id = auth.uid()
      and (created_at at time zone 'Europe/Paris')::date = v_today
  ) into v_already_posted;
  if v_already_posted then
    raise exception 'already posted today';
  end if;

  select count(*) into v_count_today
  from public.wall_notes
  where (created_at at time zone 'Europe/Paris')::date = v_today;
  if v_count_today >= 10 then
    raise exception 'wall full today';
  end if;

  insert into public.wall_notes (user_id, text, image_url)
  values (auth.uid(), trim(p_text), v_image_url)
  returning * into v_row;

  return v_row;
end;
$$;

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

-- Bucket dédié aux images du Mur (pas "avatars") : chemin d'objet
-- volontairement SANS le user_id (contrairement aux avatars) pour ne
-- jamais faire fuiter l'auteur via l'URL publique de l'image — cohérent
-- avec l'anonymat de wall_notes_today. file_size_limit + allowed_mime_types
-- bornent le stockage côté Supabase (coût) ; le client compresse déjà
-- l'image bien en dessous de cette limite avant l'upload
-- (resizeImageToBlob), qui n'est qu'un filet de sécurité, pas la seule
-- garantie.
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values ('wall-images', 'wall-images', true, 3145728, array['image/webp', 'image/jpeg', 'image/png'])
on conflict (id) do update set
  public = excluded.public,
  file_size_limit = excluded.file_size_limit,
  allowed_mime_types = excluded.allowed_mime_types;

drop policy if exists "Wall images are publicly viewable" on storage.objects;
create policy "Wall images are publicly viewable"
  on storage.objects for select
  using (bucket_id = 'wall-images');

drop policy if exists "Citizens can upload wall images" on storage.objects;
create policy "Citizens can upload wall images"
  on storage.objects for insert
  to authenticated
  with check (
    bucket_id = 'wall-images'
    and exists (select 1 from public.profiles p where p.id = auth.uid() and p.role <> 'judge')
  );

-- Réactions sur les notes -------------------------------------------------
-- Même principe d'anonymat que les notes elles-mêmes : chacun ne voit que
-- ses propres réactions (pour savoir lesquelles sont déjà activées), le
-- compte agrégé par note et par emoji est exposé séparément par
-- wall_note_reaction_counts(), sans jamais révéler qui a réagi.
create table if not exists public.wall_note_reactions (
  id bigint generated always as identity primary key,
  note_id uuid not null references public.wall_notes (id) on delete cascade,
  user_id uuid not null references public.profiles (id) on delete cascade,
  emoji text not null check (emoji in ('👍', '😂', '😱', '❤️', '🔥')),
  created_at timestamptz not null default now(),
  unique (note_id, user_id, emoji)
);

alter table public.wall_note_reactions enable row level security;
revoke insert, update, delete on public.wall_note_reactions from authenticated, anon;
grant select on public.wall_note_reactions to authenticated;

drop policy if exists "wall_note_reactions readable by reactor only" on public.wall_note_reactions;
create policy "wall_note_reactions readable by reactor only"
  on public.wall_note_reactions for select to authenticated
  using (user_id = auth.uid());

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

create or replace function public.wall_note_reaction_counts()
returns table (note_id uuid, emoji text, count bigint)
language sql
security definer
set search_path = public
as $$
  select r.note_id, r.emoji, count(*) as count
  from public.wall_note_reactions r
  join public.wall_notes w on w.id = r.note_id
  where (w.created_at at time zone 'Europe/Paris')::date = (now() at time zone 'Europe/Paris')::date
  group by r.note_id, r.emoji;
$$;

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

-- Bascule une réaction (ajoute si absente, retire si déjà posée) en un seul
-- aller-retour : un delete puis, si rien n'a été supprimé, un insert —
-- évite une lecture préalable puis un choix côté client entre 2 RPC.
create or replace function public.toggle_wall_note_reaction(p_note_id uuid, p_emoji text)
returns boolean
language plpgsql
security definer
set search_path = public
as $$
declare
  v_allowed constant text[] := array['👍', '😂', '😱', '❤️', '🔥'];
  v_deleted int;
begin
  if auth.uid() is null then
    raise exception 'authentication required';
  end if;

  if not (p_emoji = any(v_allowed)) then
    raise exception 'emoji non autorisé';
  end if;

  if not exists (select 1 from public.wall_notes where id = p_note_id) then
    raise exception 'note introuvable';
  end if;

  delete from public.wall_note_reactions
  where note_id = p_note_id and user_id = auth.uid() and emoji = p_emoji;
  get diagnostics v_deleted = row_count;

  if v_deleted > 0 then
    return false;
  end if;

  insert into public.wall_note_reactions (note_id, user_id, emoji)
  values (p_note_id, auth.uid(), p_emoji);
  return true;
end;
$$;

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

-- 15. L'Urne de l'Agora -------------------------------------------------------
-- Vote quotidien inspiré du vote à l'urne de l'Athènes antique : chaque jour
-- chaque Citoyen dépose un jeton sur une personne de son choix. Contrairement
-- au Mur de la Honte, les jetons s'accumulent sur toute la semaine dans un
-- classement public et cumulatif — identique pour tout le monde, Archontes
-- compris : contrairement à chariot_submissions/wall_notes, les Archontes
-- n'ont ici AUCUNE vision privilégiée sur qui a voté pour qui, seul le total
-- par personne est public. Les Archontes ne peuvent ni voter ni recevoir de
-- jetons. Chaque vote peut porter une justification facultative, exposée de
-- façon anonyme (urn_vote_reasons) : le contenu est public, jamais l'auteur.

create table if not exists public.urn_votes (
  id bigint generated always as identity primary key,
  voter_id uuid not null references public.profiles (id) on delete cascade,
  target_id uuid not null references public.profiles (id) on delete cascade,
  created_at timestamptz not null default now()
);

-- Justification facultative laissée par le votant. Exposée ensuite de façon
-- anonyme via urn_vote_reasons() (voir plus bas) : le contenu est public,
-- jamais son auteur — cohérent avec le reste de la table.
alter table public.urn_votes add column if not exists reason text;

alter table public.urn_votes enable row level security;
revoke insert, update, delete on public.urn_votes from authenticated, anon;
grant select on public.urn_votes to authenticated;

-- Pas d'exception juge ici (contrairement à chariot_submissions/wall_notes) :
-- même les Archontes ne voient que leur propre ligne (pour savoir "j'ai déjà
-- voté aujourd'hui") — qui a voté pour qui ne regarde personne d'autre.
drop policy if exists "urn_votes readable by voter only" on public.urn_votes;
create policy "urn_votes readable by voter only"
  on public.urn_votes for select to authenticated
  using (voter_id = auth.uid());

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

-- Seul moyen de calculer un total public par personne sans jamais exposer
-- une ligne individuelle (qui a voté pour qui) à qui que ce soit. Agrégé sur
-- tout l'historique (pas de filtre de date, contrairement à wall_notes_today
-- qui ne montre que le jour courant) : les jetons s'accumulent toute la
-- semaine.
create or replace function public.urn_vote_counts()
returns table (target_id uuid, votes bigint)
language sql
security definer
set search_path = public
as $$
  select v.target_id, count(*) as votes
  from public.urn_votes v
  group by v.target_id;
$$;

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

-- Permet de comprendre pourquoi les gens ont voté pour quelqu'un sans jamais
-- révéler qui a voté quoi (même principe que wall_notes_today : le contenu
-- est exposé, l'identité du votant jamais) — visible par tous depuis le
-- classement, Archontes compris (même vision que pour urn_vote_counts).
create or replace function public.urn_vote_reasons(p_target_id uuid)
returns table (reason text, created_at timestamptz)
language sql
security definer
set search_path = public
as $$
  select v.reason, v.created_at
  from public.urn_votes v
  where v.target_id = p_target_id
    and v.reason is not null
  order by v.created_at desc;
$$;

grant execute on function public.urn_vote_reasons(uuid) to authenticated;

-- Seul point d'entrée pour voter. Un vote par Citoyen par jour, définitif
-- (aucune RPC de modification/suppression). La signature change (ajout de
-- p_reason) : l'ancienne (uuid) est explicitement supprimée, sinon
-- create or replace créerait une 2e surcharge au lieu de remplacer celle-ci.
drop function if exists public.cast_urn_vote(uuid);

create or replace function public.cast_urn_vote(p_target_id uuid, p_reason text default null)
returns public.urn_votes
language plpgsql
security definer
set search_path = public
as $$
declare
  v_caller_role text;
  v_target_role text;
  v_today date := (now() at time zone 'Europe/Paris')::date;
  v_already_voted boolean;
  v_reason text := nullif(trim(coalesce(p_reason, '')), '');
  v_row public.urn_votes;
begin
  if auth.uid() is null then
    raise exception 'authentication required';
  end if;

  select role into v_caller_role from public.profiles where id = auth.uid();
  if v_caller_role = 'judge' then
    raise exception 'only citizens can vote';
  end if;

  if p_target_id = auth.uid() then
    raise exception 'cannot vote for yourself';
  end if;

  select role into v_target_role from public.profiles where id = p_target_id;
  if v_target_role is null then
    raise exception 'target member not found';
  end if;
  if v_target_role = 'judge' then
    raise exception 'cannot vote for a judge';
  end if;

  if v_reason is not null and length(v_reason) > 200 then
    raise exception 'justification trop longue';
  end if;

  select exists (
    select 1 from public.urn_votes
    where voter_id = auth.uid()
      and (created_at at time zone 'Europe/Paris')::date = v_today
  ) into v_already_voted;
  if v_already_voted then
    raise exception 'already voted today';
  end if;

  insert into public.urn_votes (voter_id, target_id, reason)
  values (auth.uid(), p_target_id, v_reason)
  returning * into v_row;

  return v_row;
end;
$$;

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