-- t-paliad-223 Slice A: Project Admin role on project_teams.responsibility +
-- inheritable role-edit gate.
--
-- Design: docs/design-team-admin-rework-2026-05-20.md (gauss, m-locked
-- 2026-05-20 via head's "all R approved").
--
-- Adds a fifth 'admin' value to the project_teams.responsibility enum
-- (orthogonal to the profession-driven approval ladder — admin does NOT
-- open the 4-Augen gate by itself). Introduces paliad.effective_project_admin
-- which mirrors paliad.can_see_project's shape and walks the ltree path
-- to compute inheritance. Replaces the three write-side RLS policies on
-- paliad.project_teams so role edits are gated on the new predicate
-- instead of "anyone with visibility".
--
-- Day-1 deploy = no behaviour change for callers who never use the admin
-- value: existing lead/member/observer/external rows keep their meaning,
-- and the global_admin shortcut + self-join INSERT / self-DELETE remain
-- intact.
--
-- Sections:
--   1. ALTER project_teams.responsibility CHECK to include 'admin'.
--   2. CREATE paliad.effective_project_admin(uuid, uuid).
--   3. Replace project_teams_update policy: gated on effective_project_admin.
--   4. Replace project_teams_insert policy: self-join OR effective_project_admin.
--   5. Replace project_teams_delete policy: self / global_admin / effective_project_admin.

-- ============================================================================
-- 1. Extend responsibility CHECK to include 'admin'.
--
-- 'admin' inherits down the project tree (see effective_project_admin in §2).
-- A user marked admin on a Mandant-level project is implicitly admin on
-- every Litigation / Patent / Case descendant — same shape as how 'lead'
-- already inherits.
-- ============================================================================

ALTER TABLE paliad.project_teams
    DROP CONSTRAINT IF EXISTS project_teams_responsibility_check;

ALTER TABLE paliad.project_teams
    ADD CONSTRAINT project_teams_responsibility_check
    CHECK (responsibility IN ('admin', 'lead', 'member', 'observer', 'external'));

COMMENT ON COLUMN paliad.project_teams.responsibility IS
    'Per-project responsibility. admin = can manage team + roles on this '
    'project and descendants (inherited via paliad.effective_project_admin). '
    'lead/member open the 4-Augen approval gate; observer/external close it. '
    'admin is orthogonal to the approval gate — it does NOT open it by itself.';

-- ============================================================================
-- 2. paliad.effective_project_admin(_user_id, _project_id)
--
-- Mirrors paliad.can_see_project: STABLE SECURITY DEFINER, ltree path-walk
-- against projects.path. Two branches:
--   (a) global_admin short-circuit — firm-wide admins are always admin.
--   (b) ancestor-or-self project_teams row with responsibility='admin'.
--
-- Used by the project_teams_update / _insert / _delete policies below
-- and by ProjectService for the effective_admin payload field.
--
-- The ltree-array cast is the same pattern can_see_project uses; the
-- existing GiST index on projects.path is the load-bearing index. No new
-- index needed.
-- ============================================================================

CREATE OR REPLACE FUNCTION paliad.effective_project_admin(_user_id uuid, _project_id uuid)
RETURNS boolean
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path TO 'paliad', 'public'
AS $$
    SELECT EXISTS (
        SELECT 1 FROM paliad.users u
         WHERE u.id = _user_id
           AND u.global_role = 'global_admin'
    )
    OR EXISTS (
        SELECT 1
          FROM paliad.projects      target
          JOIN paliad.project_teams pt
            ON pt.user_id = _user_id
           AND pt.responsibility = 'admin'
           AND pt.project_id = ANY(string_to_array(target.path, '.')::uuid[])
         WHERE target.id = _project_id
    );
$$;

COMMENT ON FUNCTION paliad.effective_project_admin(uuid, uuid) IS
    'True iff the user is global_admin OR has responsibility=admin on the '
    'project itself or any ancestor in the materialised ltree path. '
    'Drives the role-edit gate on project_teams (UPDATE/INSERT/DELETE RLS).';

-- ============================================================================
-- 3. project_teams_update policy: gated on effective_project_admin.
--
-- Before: USING + CHECK = can_see_project (anyone with visibility could
-- edit anyone's responsibility — the load-bearing gap that t-paliad-223
-- closes).
-- After: USING + CHECK = effective_project_admin (only project-admins
-- and global_admins can change roles).
-- ============================================================================

DROP POLICY IF EXISTS project_teams_update ON paliad.project_teams;

CREATE POLICY project_teams_update
    ON paliad.project_teams FOR UPDATE
    USING (paliad.effective_project_admin(auth.uid(), project_id))
    WITH CHECK (paliad.effective_project_admin(auth.uid(), project_id));

-- ============================================================================
-- 4. project_teams_insert policy: self-join OR effective_project_admin.
--
-- The self-join branch (user_id = auth.uid()) preserves the legacy
-- creator-as-lead INSERT in ProjectService.Create: the project creator
-- auto-joins their own project with responsibility='lead' before any
-- admin exists. Without this branch, the first-ever team row on a new
-- project would fail because no admin has been granted yet.
--
-- For all other inserts (adding other users), the caller must be an
-- effective_project_admin on the target project.
-- ============================================================================

DROP POLICY IF EXISTS project_teams_insert ON paliad.project_teams;

CREATE POLICY project_teams_insert
    ON paliad.project_teams FOR INSERT
    WITH CHECK (
        user_id = auth.uid()
        OR paliad.effective_project_admin(auth.uid(), project_id)
    );

-- ============================================================================
-- 5. project_teams_delete policy: self / global_admin / effective_project_admin.
--
-- Additive: self-remove + global_admin still work; project-admin can now
-- also remove members.
-- ============================================================================

DROP POLICY IF EXISTS project_teams_delete ON paliad.project_teams;

CREATE POLICY project_teams_delete
    ON paliad.project_teams FOR DELETE
    USING (
        paliad.can_see_project(project_id)
        AND (
            user_id = auth.uid()
            OR EXISTS (
                SELECT 1 FROM paliad.users u
                 WHERE u.id = auth.uid()
                   AND u.global_role = 'global_admin'
            )
            OR paliad.effective_project_admin(auth.uid(), project_id)
        )
    );
