-- Alpenwerk HR — initial schema
--
-- Based on spec §3, with the following corrections
-- (see the Phase 1 plan for the full rationale):
--   - gender_type restricted to m/w (spec's domain rules exclude "divers")
--   - employees.location replaced by a locations reference table + location_id FK
--     (the spec's own CHECK listed Wien/Linz/Graz, which contradicts §2's real site list)
--   - nationality constrained to the picklist named in §2
--   - added: locations, profiles (role-based access), employees_directory (salary-masked view)
--   - added: address/address_country/contract_end_date columns (required by §4.5's "Daten ändern"
--     panel and the befristet/"Befristet bis" rule, but missing from §3's literal table)
--   - added: 'Stammdatenänderung' history event type (required by §4.5, missing from §3's enum)
--   - added: reorg_scenarios.undo_snapshot jsonb (required by §4.7's undo feature)
--   - added: position number generation + org-unit auto-derivation as real functions/triggers

create extension if not exists "pgcrypto";

-- ── Org units ────────────────────────────────────────────────
create table divisions ( -- "Bereich", numbers 20xxxxxx
  id uuid primary key default gen_random_uuid(),
  org_number text not null unique check (org_number ~ '^20\d{6}$'),
  name text not null unique
);

create table departments ( -- "Abteilung", numbers 21xxxxxx
  id uuid primary key default gen_random_uuid(),
  org_number text not null unique check (org_number ~ '^21\d{6}$'),
  name text not null,
  division_id uuid not null references divisions(id)
);

create table teams ( -- "Team", numbers 22xxxxxx
  id uuid primary key default gen_random_uuid(),
  org_number text not null unique check (org_number ~ '^22\d{6}$'),
  name text not null,
  department_id uuid not null references departments(id)
);

-- ── Locations (site ties to a country; country picklist drives the UI's
--    "select country auto-selects its locations" rule from §2) ─────────
create table locations (
  id uuid primary key default gen_random_uuid(),
  name text not null unique, -- Wien-Hernals, Wolkersdorf, Köln, Brünn, Ljubljana
  country text not null check (country in ('Österreich', 'Deutschland', 'Tschechien', 'Slowenien'))
);

-- ── Profiles (role-based access per §6) ─────────────────────
create table profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  email text not null,
  full_name text,
  role text not null default 'manager' check (role in ('hr_admin', 'manager')),
  created_at timestamptz not null default now()
);

-- ── Employees ────────────────────────────────────────────────
create type employment_status as enum ('Aktiv', 'Karenz', 'Geplant', 'Ausgetreten');
create type employment_type as enum ('Vollzeit', 'Teilzeit');
create type contract_type as enum ('unbefristet', 'befristet');
create type paygrade_type as enum ('A', 'B', 'C', 'D', 'E', 'F');
create type source_type as enum ('Intern', 'Extern');
create type gender_type as enum ('m', 'w');

create table employees (
  id uuid primary key default gen_random_uuid(),
  personnel_number int generated always as identity (start with 1001), -- Pers.-Nr.
  first_name text not null,
  last_name text not null,
  gender gender_type not null,
  birth_date date not null,
  sv_nummer text,
  nationality text not null default 'Österreich' check (nationality in (
    'Österreich', 'Deutschland', 'Tschechien', 'Slowenien', 'Türkei',
    'Serbien', 'Kroatien', 'Bosnien', 'Ungarn', 'Andere'
  )),
  address text,
  address_country text check (address_country in ('Österreich', 'Deutschland', 'Tschechien', 'Slowenien', 'Andere')),
  email text not null unique,
  phone text,
  team_id uuid references teams(id), -- nullable only for CEO / division heads without a team
  division_id uuid not null references divisions(id), -- auto-derived from team_id by trigger when team_id is set
  job_title text not null,
  location_id uuid not null references locations(id),
  manager_id uuid references employees(id),
  org_level int not null default 3 check (org_level between 0 and 3), -- 0=CEO,1=division head,2=team lead,3=IC
  is_lead boolean not null default false,
  employment_type employment_type not null default 'Vollzeit',
  weekly_hours numeric(4,1) not null default 38.5,
  monthly_salary_gross numeric(10,2) not null check (monthly_salary_gross > 0), -- 14x/year convention
  contract_type contract_type not null default 'unbefristet',
  contract_end_date date,
  paygrade paygrade_type not null default 'B',
  source source_type not null default 'Extern',
  status employment_status not null default 'Aktiv',
  entry_date date not null,
  exit_date date,
  exit_reason text,
  karenz_return_date date,
  avatar_color text, -- hex, for initials badge; app falls back to a deterministic hash if null
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint chk_exit_after_entry check (exit_date is null or exit_date >= entry_date),
  constraint chk_karenz_return_after_entry check (karenz_return_date is null or karenz_return_date >= entry_date),
  constraint chk_befristet_end check (contract_type <> 'befristet' or contract_end_date is not null),
  constraint chk_weekly_hours check (
    (employment_type = 'Vollzeit' and weekly_hours = 38.5) or
    (employment_type = 'Teilzeit' and weekly_hours > 0 and weekly_hours < 38.5)
  )
);
create index on employees (team_id);
create index on employees (division_id);
create index on employees (manager_id);
create index on employees (status);

-- ── Employee history (append-only audit trail per person) ───
create type history_event_type as enum (
  'Eintritt', 'Beförderung', 'Versetzung', 'Karenz', 'Vertragsänderung', 'Stammdatenänderung',
  'Austritt', 'Wiedereintritt', 'Reorganisation', 'Gehaltsanpassung', 'Rückkehr'
);
create table employee_history (
  id uuid primary key default gen_random_uuid(),
  employee_id uuid not null references employees(id) on delete cascade,
  event_date date not null,
  event_type history_event_type not null,
  description text not null,
  created_at timestamptz not null default now()
);
create index on employee_history (employee_id, event_date desc);

-- ── Positions (Planstellen) ──────────────────────────────────
create table positions (
  id uuid primary key default gen_random_uuid(),
  position_number text not null unique check (position_number ~ '^6\d{7}$'),
  title text not null,
  team_id uuid not null references teams(id),
  division_id uuid not null references divisions(id), -- auto-derived from team_id by trigger
  is_lead boolean not null default false,
  reports_to_employee_id uuid references employees(id), -- the superior manager chosen at creation
  status text not null default 'open' check (status in ('open', 'filled')),
  created_at timestamptz not null default now(),
  filled_at timestamptz,
  filled_by_employee_id uuid references employees(id)
);
create index on positions (team_id);
create index on positions (status);

-- ── Hire drafts (resumable wizard state) ─────────────────────
create table hire_drafts (
  id uuid primary key default gen_random_uuid(),
  created_by uuid references auth.users(id),
  step int not null default 0,
  payload jsonb not null, -- full wizard form state
  updated_at timestamptz not null default now()
);

-- ── Saved reports ────────────────────────────────────────────
create table saved_reports (
  id uuid primary key default gen_random_uuid(),
  created_by uuid references auth.users(id),
  name text not null,
  config jsonb not null, -- { measure, group, split, filters... }
  created_at timestamptz not null default now()
);

-- ── Audit log (system-wide, immutable) ───────────────────────
create table audit_log (
  id uuid primary key default gen_random_uuid(),
  occurred_at timestamptz not null default now(),
  actor_user_id uuid references auth.users(id),
  actor_name text not null,
  action text not null, -- e.g. 'Neueinstellung','Austritt','Versetzung','Beförderung','Karenz',
                         -- 'Vertragsänderung','Stammdatenänderung','Wiedereinstellung','Ausschreibung',
                         -- 'Interne Besetzung','Reorganisation','Reorganisation rückgängig','Rückkehr',
                         -- 'Gehaltsanpassung'
  target_label text not null, -- human-readable name of what changed
  target_employee_id uuid references employees(id),
  details text
);
create index on audit_log (occurred_at desc);

-- ── Reorg scenarios (persistence of in-progress/applied reorg plans) ─
create table reorg_scenarios (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  effective_date date not null,
  created_by uuid references auth.users(id),
  applied boolean not null default false,
  applied_at timestamptz,
  undo_snapshot jsonb, -- pre-change employee state + history/audit high-water marks, for undo
  created_at timestamptz not null default now()
);
create table reorg_moves (
  id uuid primary key default gen_random_uuid(),
  scenario_id uuid not null references reorg_scenarios(id) on delete cascade,
  kind text not null check (kind in ('emp', 'team', 'abt', 'dept')),
  payload jsonb not null -- employee ids / team id / dept id / target division, counts, labels
);

-- ── Functions & triggers ─────────────────────────────────────

-- Auto-derive division_id from team_id (keeps the denormalized division in sync
-- with the team's real parent chain; §3's closing instruction).
create or replace function fn_set_employee_org_unit()
returns trigger
language plpgsql
as $$
begin
  if new.team_id is not null then
    select dep.division_id into new.division_id
    from teams t
    join departments dep on dep.id = t.department_id
    where t.id = new.team_id;
  end if;
  return new;
end;
$$;

create trigger trg_employees_set_org_unit
before insert or update of team_id on employees
for each row execute function fn_set_employee_org_unit();

create or replace function fn_set_position_org_unit()
returns trigger
language plpgsql
as $$
begin
  select dep.division_id into new.division_id
  from teams t
  join departments dep on dep.id = t.department_id
  where t.id = new.team_id;
  return new;
end;
$$;

create trigger trg_positions_set_org_unit
before insert or update of team_id on positions
for each row execute function fn_set_position_org_unit();

create or replace function fn_touch_updated_at()
returns trigger
language plpgsql
as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

create trigger trg_employees_touch_updated_at
before update on employees
for each row execute function fn_touch_updated_at();

-- Unique 8-digit position numbers starting with '6' (§2).
create or replace function generate_position_number()
returns text
language plpgsql
as $$
declare
  candidate text;
begin
  loop
    candidate := '6' || lpad(floor(random() * 10000000)::text, 7, '0');
    exit when not exists (select 1 from positions where position_number = candidate);
  end loop;
  return candidate;
end;
$$;

alter table positions alter column position_number set default generate_position_number();

-- ── Role helper (SECURITY DEFINER avoids RLS recursion on profiles) ──
create or replace function is_hr_admin()
returns boolean
language sql
security definer
set search_path = public
stable
as $$
  select exists (
    select 1 from profiles p where p.id = auth.uid() and p.role = 'hr_admin'
  );
$$;

-- ── Salary-masked read view for the manager role (§6) ────────
-- Owned by the migration role (postgres), which bypasses RLS on the base
-- table, so this view is reachable by both roles while column-masking
-- salary per session via is_hr_admin().
create view employees_directory as
select
  e.id, e.personnel_number, e.first_name, e.last_name, e.gender, e.birth_date, e.sv_nummer,
  e.nationality, e.address, e.address_country, e.email, e.phone, e.team_id, e.division_id,
  e.job_title, e.location_id, e.manager_id, e.org_level, e.is_lead, e.employment_type,
  e.weekly_hours,
  case when is_hr_admin() then e.monthly_salary_gross else null end as monthly_salary_gross,
  e.contract_type, e.contract_end_date, e.paygrade, e.source, e.status, e.entry_date, e.exit_date,
  e.exit_reason, e.karenz_return_date, e.avatar_color, e.created_at, e.updated_at
from employees e;

grant select on employees_directory to authenticated;

-- ── Row Level Security ───────────────────────────────────────
alter table divisions enable row level security;
alter table departments enable row level security;
alter table teams enable row level security;
alter table locations enable row level security;
alter table profiles enable row level security;
alter table employees enable row level security;
alter table employee_history enable row level security;
alter table positions enable row level security;
alter table hire_drafts enable row level security;
alter table saved_reports enable row level security;
alter table audit_log enable row level security;
alter table reorg_scenarios enable row level security;
alter table reorg_moves enable row level security;

-- Org reference data: readable by any authenticated user, writable by hr_admin only.
create policy "org_read" on divisions for select using (auth.role() = 'authenticated');
create policy "org_write" on divisions for all using (is_hr_admin()) with check (is_hr_admin());
create policy "org_read" on departments for select using (auth.role() = 'authenticated');
create policy "org_write" on departments for all using (is_hr_admin()) with check (is_hr_admin());
create policy "org_read" on teams for select using (auth.role() = 'authenticated');
create policy "org_write" on teams for all using (is_hr_admin()) with check (is_hr_admin());
create policy "org_read" on locations for select using (auth.role() = 'authenticated');
create policy "org_write" on locations for all using (is_hr_admin()) with check (is_hr_admin());

-- Profiles: users read their own row; hr_admin reads/writes all.
create policy "profiles_select_own" on profiles for select using (auth.uid() = id);
create policy "profiles_select_admin" on profiles for select using (is_hr_admin());
create policy "profiles_write_admin" on profiles for insert with check (is_hr_admin());
create policy "profiles_update_admin" on profiles for update using (is_hr_admin()) with check (is_hr_admin());

-- Employees: only hr_admin reads/writes the base table directly. The manager
-- role reads through employees_directory instead (salary masked there).
create policy "employees_admin_all" on employees for all using (is_hr_admin()) with check (is_hr_admin());

-- Employee history: any authenticated user can read; only hr_admin can append; immutable otherwise.
create policy "history_read" on employee_history for select using (auth.role() = 'authenticated');
create policy "history_insert_admin" on employee_history for insert with check (is_hr_admin());

-- Positions: any authenticated user can browse open positions; hr_admin manages them.
create policy "positions_read" on positions for select using (auth.role() = 'authenticated');
create policy "positions_write_admin" on positions for all using (is_hr_admin()) with check (is_hr_admin());

-- Hire drafts: scoped to their creator.
create policy "hire_drafts_owner" on hire_drafts for all
  using (created_by = auth.uid()) with check (created_by = auth.uid());

-- Saved reports: scoped to their creator.
create policy "saved_reports_owner" on saved_reports for all
  using (created_by = auth.uid()) with check (created_by = auth.uid());

-- Audit log: any authenticated user can read; only hr_admin can append; immutable (no update/delete policy).
create policy "audit_read" on audit_log for select using (auth.role() = 'authenticated');
create policy "audit_insert_admin" on audit_log for insert with check (is_hr_admin());

-- Reorg scenarios/moves: any authenticated user can see the (small) recent list; hr_admin manages them.
create policy "reorg_scenarios_read" on reorg_scenarios for select using (auth.role() = 'authenticated');
create policy "reorg_scenarios_write_admin" on reorg_scenarios for all using (is_hr_admin()) with check (is_hr_admin());
create policy "reorg_moves_read" on reorg_moves for select using (auth.role() = 'authenticated');
create policy "reorg_moves_write_admin" on reorg_moves for all using (is_hr_admin()) with check (is_hr_admin());
