-- Schritt 1 auf dem Weg weg von Supabase: eigene Benutzertabelle und ein
-- eigener Sitzungskontext.
--
-- Ziel ist ein Schema, das auf jedem PostgreSQL ab 15 läuft — Azure Flexible
-- Server, RDS, Cloud SQL, eigenes Blech. Heute hängt genau eine Sache an
-- Supabase: `auth.uid()`, die Kennung der angemeldeten Person. Sie steckt in
-- 72 Zeilen SQL, aber für die Absicherung zählt nur eine Stelle —
-- is_hr_user(), das alle 58 RLS-Policies aufrufen.
--
-- Diese Migration ist bewusst **additiv und beidseitig lauffähig**: die
-- Anwendung läuft danach unverändert auf Supabase weiter, während die neue
-- Zugriffsschicht daneben entsteht. Ein Umbau, der beide Enden gleichzeitig
-- bewegt, lässt sich nicht testen.

-- ═══ 1. Sitzungskontext ══════════════════════════════════════════
-- Wer gerade angemeldet ist, kommt künftig aus einer Sitzungsvariablen, die
-- die Zugriffsschicht **transaktionslokal** setzt (siehe lib/db).
--
-- Warum plpgsql und nicht `language sql`: eine SQL-Funktion wird beim Anlegen
-- geparst, und `auth.uid()` existiert auf einem gewöhnlichen PostgreSQL
-- nicht — die Funktion liesse sich dort gar nicht erst erzeugen. plpgsql löst
-- den Aufruf erst zur Laufzeit auf, und der Ausnahmeblock fängt die fehlende
-- Funktion ab. Genau das macht diese Migration auf beiden Systemen anwendbar.
create or replace function app_current_user_id()
returns uuid
language plpgsql
stable
security definer
set search_path = public, pg_temp
as $$
declare
  v_id uuid;
begin
  -- Vorrang hat der eigene Kontext. `true` als zweites Argument heisst:
  -- fehlt die Variable, kommt null statt eines Fehlers.
  v_id := nullif(current_setting('app.user_id', true), '')::uuid;
  if v_id is not null then
    return v_id;
  end if;

  -- Übergangsweise: solange die Anmeldung noch über GoTrue läuft. Fällt in
  -- der Abschlussmigration weg, zusammen mit den Fremdschlüsseln auf
  -- auth.users.
  begin
    execute 'select auth.uid()' into v_id;
  exception
    when undefined_function or invalid_schema_name or undefined_table then
      v_id := null;
  end;
  return v_id;
end;
$$;

comment on function app_current_user_id() is
  'Kennung der angemeldeten Person: erst app.user_id aus der Sitzung, ersatzweise auth.uid(). Der zweite Zweig ist Übergang.';

-- Zugeteilt wird nur an Rollen, die es auch gibt. Auf einem gewöhnlichen
-- PostgreSQL existieren anon/authenticated/service_role nicht, und ein
-- `grant` auf eine unbekannte Rolle bricht die Migration ab — dieselbe Datei
-- liefe dort also nicht. Genau das soll sie aber.
do $$
declare r text;
begin
  foreach r in array array['anon', 'authenticated', 'service_role'] loop
    if exists (select 1 from pg_roles where rolname = r) then
      execute format('grant execute on function app_current_user_id() to %I', r);
    end if;
  end loop;
end;
$$;

-- ═══ 2. Benutzertabelle ══════════════════════════════════════════
-- Tritt an die Stelle von auth.users. Die neun Fremdschlüssel, die heute
-- dorthin zeigen, wandern in der Abschlussmigration hierher.
create table if not exists app_users (
  id uuid primary key default gen_random_uuid(),
  -- Die `oid` aus dem Entra-Token. Unveränderlich, anders als die E-Mail:
  -- eine Namensänderung darf nicht zu einem neuen Konto führen.
  external_id text not null unique,
  email text not null,
  full_name text,
  created_at timestamptz not null default now(),
  last_seen_at timestamptz
);

comment on table app_users is
  'Ersetzt auth.users. external_id ist die oid des Identitätsanbieters, nicht die E-Mail.';

create index if not exists app_users_email_idx on app_users (lower(email));

alter table app_users enable row level security;

-- Sich selbst sehen darf jede:r Angemeldete; alles andere ist HR-Sache.
-- Ohne diese Policy käme die Anmeldung nicht an die eigene Zeile.
drop policy if exists "app_users_select_own" on app_users;
create policy "app_users_select_own" on app_users
  for select using (id = app_current_user_id() or is_hr_user());

do $$
begin
  if exists (select 1 from pg_roles where rolname = 'anon') then
    execute 'grant select on table app_users to anon';
  end if;
  if exists (select 1 from pg_roles where rolname = 'authenticated') then
    execute 'grant select on table app_users to authenticated';
  end if;
  if exists (select 1 from pg_roles where rolname = 'service_role') then
    execute 'grant all on table app_users to service_role';
  end if;
end;
$$;

-- ═══ 3. Die eine Brücke umlegen ══════════════════════════════════
-- Ab hier fragt die Absicherung nicht mehr Supabase, sondern den eigenen
-- Kontext. Die 58 Policies bleiben Wort für Wort unverändert — sie rufen
-- weiterhin is_hr_user() auf und merken davon nichts.
create or replace function is_hr_user()
returns boolean
language sql
security definer
set search_path = public, pg_temp
stable
as $$
  select exists (
    select 1 from profiles p
    where p.id = app_current_user_id() and p.role = 'hr' and p.is_active = true
  );
$$;

create or replace function current_hr_user_id()
returns uuid
language sql
security definer
set search_path = public, pg_temp
stable
as $$
  select p.id from profiles p
  where p.id = app_current_user_id() and p.role = 'hr' and p.is_active = true;
$$;

create or replace function current_actor_name()
returns text
language sql
stable
set search_path = public, pg_temp
as $$
  select coalesce(p.full_name, p.email, 'Unbekannt')
  from profiles p where p.id = app_current_user_id();
$$;

-- ═══ 4. Die fünf Policies mit direktem auth.uid() ════════════════
-- Die übrigen 53 laufen über is_hr_user() und brauchen nichts.
drop policy if exists "profiles_select_own" on profiles;
create policy "profiles_select_own" on profiles
  for select using (app_current_user_id() = id);

-- Die Namen stammen aus 20260714120000_hr_only_access.sql: „_owner", nicht
-- „_own". Mit dem falschen Namen bricht die Migration bei create policy ab.
drop policy if exists "hire_drafts_owner" on hire_drafts;
create policy "hire_drafts_owner" on hire_drafts
  for all
  using (created_by = app_current_user_id() and is_hr_user())
  with check (created_by = app_current_user_id() and is_hr_user());

drop policy if exists "saved_reports_owner" on saved_reports;
create policy "saved_reports_owner" on saved_reports
  for all
  using (created_by = app_current_user_id() and is_hr_user())
  with check (created_by = app_current_user_id() and is_hr_user());

-- ═══ 5. Gegenprobe ═══════════════════════════════════════════════
-- Ohne Kontext und ohne Anmeldung darf is_hr_user() nicht wahr sein. Das
-- klingt selbstverständlich und ist genau der Fehler, der eine ganze
-- Datenbank öffnet.
do $$
begin
  perform set_config('app.user_id', '', true);
  if is_hr_user() then
    raise exception 'is_hr_user() liefert ohne Sitzungskontext true — Abbruch.';
  end if;

  perform set_config('app.user_id', gen_random_uuid()::text, true);
  if is_hr_user() then
    raise exception 'is_hr_user() liefert für eine unbekannte Kennung true — Abbruch.';
  end if;

  -- Aufräumen: die Einstellung gilt bis zum Ende dieser Transaktion, und
  -- was danach in derselben Sitzung läuft, soll sie nicht erben.
  perform set_config('app.user_id', '', true);
end;
$$;
