-- =====================================================================
-- Migration 0001 — Auth, organizational hierarchy, RLS foundations.
--
-- Implements roadmap Step 1 (identity & access) and Step 2 (org & master
-- data). No money tables yet; those come in 0002+. Every table is created
-- with RLS enabled and a deny-by-default posture; specific policies are
-- added inline.
--
-- Threat-model rows addressed: 9, 18, 20, 24, 25.
-- =====================================================================

-- Required extensions ---------------------------------------------------
create extension if not exists "pgcrypto";   -- gen_random_uuid, digest
create extension if not exists "citext";     -- case-insensitive text

-- Dedicated schema for app data (keeps `public` clean) ------------------
create schema if not exists app;

-- Revoke broad defaults; we will grant explicitly per role.
revoke all on schema app from public;
grant usage on schema app to authenticated;

-- =====================================================================
-- Roles
-- =====================================================================
-- We model business roles as an enum, separate from Postgres/Supabase
-- roles. Supabase still uses `authenticated`/`anon`; the business role is
-- read from `app.user_shop_assignments` per shop.
do $$ begin
  create type app.business_role as enum ('owner', 'manager', 'cashier', 'auditor');
exception when duplicate_object then null; end $$;

-- =====================================================================
-- Shops, tills, users
-- =====================================================================
create table if not exists app.shops (
  id              uuid primary key default gen_random_uuid(),
  name            text not null,
  address         text,
  omt_agent_code  text unique,
  alfa_dealer_code  text unique,
  touch_dealer_code text unique,
  created_at      timestamptz not null default now(),
  created_by      uuid references auth.users(id)
);

create table if not exists app.tills (
  id          uuid primary key default gen_random_uuid(),
  shop_id     uuid not null references app.shops(id) on delete restrict,
  name        text not null,
  -- Pin a till to a hardware device. New devices must be registered by an
  -- owner; blocks vector #20 (second undeclared till on same machine).
  device_fingerprint text unique,
  is_active   boolean not null default true,
  created_at  timestamptz not null default now(),
  unique (shop_id, name)
);

-- Profile mirror of auth.users so we can attach business attributes
-- without granting clients access to the auth schema.
create table if not exists app.user_profiles (
  user_id    uuid primary key references auth.users(id) on delete cascade,
  full_name  text not null,
  phone      text,
  -- 6-digit PIN, salted+hashed. Never store plaintext.
  pin_hash   text,
  pin_set_at timestamptz,
  is_active  boolean not null default true,
  created_at timestamptz not null default now()
);

create table if not exists app.user_shop_assignments (
  user_id   uuid not null references auth.users(id) on delete cascade,
  shop_id   uuid not null references app.shops(id)  on delete cascade,
  role      app.business_role not null,
  assigned_at timestamptz not null default now(),
  assigned_by uuid references auth.users(id),
  primary key (user_id, shop_id)
);

create index if not exists idx_assignments_shop on app.user_shop_assignments(shop_id);
create index if not exists idx_assignments_role on app.user_shop_assignments(shop_id, role);

-- =====================================================================
-- Helper functions (SECURITY DEFINER) used by RLS policies.
-- These run with the function owner's privileges, so they can read
-- assignment rows even when the calling user cannot read the table.
-- =====================================================================
create or replace function app.current_user_id()
returns uuid
language sql
stable
as $$ select auth.uid() $$;

create or replace function app.has_role_in_shop(p_shop uuid, p_role app.business_role)
returns boolean
language sql
security definer
set search_path = app, public
stable
as $$
  select exists (
    select 1
    from app.user_shop_assignments a
    where a.user_id = auth.uid()
      and a.shop_id = p_shop
      and a.role    = p_role
  );
$$;

create or replace function app.has_any_role_in_shop(p_shop uuid, p_roles app.business_role[])
returns boolean
language sql
security definer
set search_path = app, public
stable
as $$
  select exists (
    select 1
    from app.user_shop_assignments a
    where a.user_id = auth.uid()
      and a.shop_id = p_shop
      and a.role    = any(p_roles)
  );
$$;

create or replace function app.is_owner_anywhere()
returns boolean
language sql
security definer
set search_path = app, public
stable
as $$
  select exists (
    select 1 from app.user_shop_assignments a
    where a.user_id = auth.uid() and a.role = 'owner'
  );
$$;

revoke all on function app.has_role_in_shop(uuid, app.business_role)        from public;
revoke all on function app.has_any_role_in_shop(uuid, app.business_role[]) from public;
revoke all on function app.is_owner_anywhere()                              from public;
grant execute on function app.has_role_in_shop(uuid, app.business_role)         to authenticated;
grant execute on function app.has_any_role_in_shop(uuid, app.business_role[])   to authenticated;
grant execute on function app.is_owner_anywhere()                               to authenticated;

-- =====================================================================
-- PIN management. Plaintext PINs never leave the server.
-- =====================================================================
create or replace function app.set_my_pin(p_pin text)
returns void
language plpgsql
security definer
set search_path = app, public
as $$
begin
  if p_pin !~ '^[0-9]{6}$' then
    raise exception 'PIN must be exactly 6 digits';
  end if;
  insert into app.user_profiles(user_id, full_name, pin_hash, pin_set_at)
    values (auth.uid(), coalesce((select full_name from app.user_profiles where user_id = auth.uid()), 'Unnamed'),
            crypt(p_pin, gen_salt('bf', 10)), now())
  on conflict (user_id) do update
    set pin_hash = crypt(p_pin, gen_salt('bf', 10)),
        pin_set_at = now();
end;
$$;

create or replace function app.verify_my_pin(p_pin text)
returns boolean
language plpgsql
security definer
set search_path = app, public
as $$
declare h text;
begin
  select pin_hash into h from app.user_profiles where user_id = auth.uid();
  if h is null then return false; end if;
  return h = crypt(p_pin, h);
end;
$$;

revoke all on function app.set_my_pin(text)    from public;
revoke all on function app.verify_my_pin(text) from public;
grant execute on function app.set_my_pin(text)    to authenticated;
grant execute on function app.verify_my_pin(text) to authenticated;

-- =====================================================================
-- Audit log of authentication / authorization events.
-- Append-only: revoke UPDATE and DELETE; only INSERT via function.
-- =====================================================================
create table if not exists app.auth_events (
  id          bigserial primary key,
  occurred_at timestamptz not null default now(),
  user_id     uuid,
  event_type  text not null,                  -- login, pin_ok, pin_fail, role_change, device_register, ...
  shop_id     uuid,
  device_fingerprint text,
  metadata    jsonb not null default '{}'::jsonb
);

create index if not exists idx_auth_events_user on app.auth_events(user_id, occurred_at desc);
create index if not exists idx_auth_events_shop on app.auth_events(shop_id, occurred_at desc);

create or replace function app.log_auth_event(
  p_event_type text,
  p_shop uuid,
  p_device text,
  p_metadata jsonb
)
returns void
language plpgsql
security definer
set search_path = app, public
as $$
begin
  insert into app.auth_events(user_id, event_type, shop_id, device_fingerprint, metadata)
    values (auth.uid(), p_event_type, p_shop, p_device, coalesce(p_metadata, '{}'::jsonb));
end;
$$;

revoke all on function app.log_auth_event(text, uuid, text, jsonb) from public;
grant execute on function app.log_auth_event(text, uuid, text, jsonb) to authenticated;

-- =====================================================================
-- RLS — deny by default, then allow per role.
-- =====================================================================
alter table app.shops                 enable row level security;
alter table app.tills                 enable row level security;
alter table app.user_profiles         enable row level security;
alter table app.user_shop_assignments enable row level security;
alter table app.auth_events           enable row level security;

-- Force RLS even for table owners (defense in depth against insider edits,
-- threat-model row #24).
alter table app.shops                 force row level security;
alter table app.tills                 force row level security;
alter table app.user_profiles         force row level security;
alter table app.user_shop_assignments force row level security;
alter table app.auth_events           force row level security;

-- shops: owners and assigned users can see their shops.
drop policy if exists shops_select on app.shops;
create policy shops_select on app.shops
  for select to authenticated
  using (
    app.is_owner_anywhere()
    or exists (
      select 1 from app.user_shop_assignments a
      where a.shop_id = shops.id and a.user_id = auth.uid()
    )
  );

-- Only owners can create/modify shops, and never via direct UPDATE of
-- security-relevant columns; we still permit it here but real changes
-- should go through dedicated functions later.
drop policy if exists shops_write_owner on app.shops;
create policy shops_write_owner on app.shops
  for all to authenticated
  using (app.is_owner_anywhere())
  with check (app.is_owner_anywhere());

-- tills: visible to everyone assigned to the shop, writable only by owners.
drop policy if exists tills_select on app.tills;
create policy tills_select on app.tills
  for select to authenticated
  using (
    app.is_owner_anywhere()
    or exists (
      select 1 from app.user_shop_assignments a
      where a.shop_id = tills.shop_id and a.user_id = auth.uid()
    )
  );

drop policy if exists tills_write_owner on app.tills;
create policy tills_write_owner on app.tills
  for all to authenticated
  using (app.has_role_in_shop(tills.shop_id, 'owner'))
  with check (app.has_role_in_shop(tills.shop_id, 'owner'));

-- user_profiles: a user can read/update their own profile (but PIN is
-- changed only via the set_my_pin function). Owners can read all.
drop policy if exists profiles_select_self_or_owner on app.user_profiles;
create policy profiles_select_self_or_owner on app.user_profiles
  for select to authenticated
  using (user_id = auth.uid() or app.is_owner_anywhere());

drop policy if exists profiles_update_self on app.user_profiles;
create policy profiles_update_self on app.user_profiles
  for update to authenticated
  using (user_id = auth.uid())
  with check (user_id = auth.uid());

-- user_shop_assignments: a user can see their own assignments; owners can
-- see/manage all assignments in their own shops.
drop policy if exists assignments_select on app.user_shop_assignments;
create policy assignments_select on app.user_shop_assignments
  for select to authenticated
  using (
    user_id = auth.uid()
    or app.has_role_in_shop(shop_id, 'owner')
  );

drop policy if exists assignments_write_owner on app.user_shop_assignments;
create policy assignments_write_owner on app.user_shop_assignments
  for all to authenticated
  using (app.has_role_in_shop(shop_id, 'owner'))
  with check (app.has_role_in_shop(shop_id, 'owner'));

-- auth_events: nobody writes directly; only the log_auth_event function.
-- Reads: a user sees their own events; owners see all in their shops.
revoke insert, update, delete on app.auth_events from authenticated;

drop policy if exists auth_events_select on app.auth_events;
create policy auth_events_select on app.auth_events
  for select to authenticated
  using (
    user_id = auth.uid()
    or (shop_id is not null and app.has_role_in_shop(shop_id, 'owner'))
  );

-- =====================================================================
-- Hard prohibitions — no DELETE on auth_events from anyone (including
-- service role used by the app). Only DBA at psql can DELETE, and that
-- itself should be audited at the infrastructure level.
-- =====================================================================
revoke delete on app.auth_events from authenticated;
-- Note: in Supabase, the `service_role` bypasses RLS but still respects
-- table grants. Revoke explicitly:
do $$ begin
  if exists (select 1 from pg_roles where rolname = 'service_role') then
    execute 'revoke delete on app.auth_events from service_role';
    execute 'revoke update on app.auth_events from service_role';
  end if;
end $$;

-- =====================================================================
-- Grants for ordinary table access (RLS still applies).
-- =====================================================================
grant select                         on app.shops                 to authenticated;
grant insert, update                 on app.shops                 to authenticated;
grant select                         on app.tills                 to authenticated;
grant insert, update                 on app.tills                 to authenticated;
grant select, update                 on app.user_profiles         to authenticated;
grant select, insert, update, delete on app.user_shop_assignments to authenticated;
grant select                         on app.auth_events           to authenticated;

-- End migration 0001 ----------------------------------------------------
