-- t-paliad-139: hierarchy aggregation — partner-unit derivation schema.
--
-- Design: docs/design-hierarchy-aggregation-2026-05-06.md (noether, m-locked 2026-05-06).
--
-- This is the Phase 2 schema migration. Day-1 deploy = zero behaviour change
-- because:
--   - Every existing partner_unit_members row defaults to unit_role='attorney'.
--   - The default derive_unit_roles on the new junction is {'pa','senior_pa'}.
--   - No project_partner_units rows exist yet; admins opt-in by attaching
--     units to projects.
-- Until those two conditions diverge, no derivation happens and visibility
-- behaves identically to the pre-055 world.
--
-- Sections:
--   1. ALTER paliad.partner_unit_members ADD COLUMN unit_role.
--   2. CREATE paliad.project_partner_units junction (with RLS).
--   3. CREATE paliad.approval_role_from_unit_role helper.
--   4. CREATE OR REPLACE paliad.can_see_project — extended with derivation
--      branch.

-- ============================================================================
-- 1. unit_role on paliad.partner_unit_members.
--
-- Per-unit role distinction so derivation can target specific tiers (default
-- {pa, senior_pa}) without re-introducing a firm-wide rank column. The same
-- user can have a different unit_role in different units; in practice most
-- users belong to one unit so this is effectively a firm-rank, but the per-
-- unit framing preserves the t-paliad-051/-138 three-axis principle on the
-- user side (job_title remains free-text display, global_role stays
-- standard|global_admin).
-- ============================================================================

ALTER TABLE paliad.partner_unit_members
    ADD COLUMN unit_role text NOT NULL DEFAULT 'attorney'
    CHECK (unit_role IN ('lead', 'attorney', 'senior_pa', 'pa', 'paralegal'));

-- ============================================================================
-- 2. paliad.project_partner_units — project ↔ unit involvement.
--
-- A row here means "this unit is involved on this project, and the listed
-- unit_roles auto-derive onto the project team". Authority defaults to off
-- (visibility-only): set derive_grants_authority=true to let derived members
-- count as approvers (per t-paliad-139 §3.4). Composite PK enforces "one
-- attachment per (project, unit)".
-- ============================================================================

CREATE TABLE paliad.project_partner_units (
    project_id              uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE,
    partner_unit_id         uuid NOT NULL REFERENCES paliad.partner_units(id) ON DELETE CASCADE,
    -- Roles in the unit that auto-derive onto the project team. Defaults
    -- target PAs only; a project can widen to ['pa','senior_pa','attorney']
    -- to pull the whole unit, or narrow to ['pa'] to exclude senior_pa.
    derive_unit_roles       text[] NOT NULL DEFAULT ARRAY['pa', 'senior_pa'],
    -- Strict default: derived members are visibility-only. Flipping this on
    -- lets them be eligible approvers per the t-138 ladder via the mapping
    -- in paliad.approval_role_from_unit_role.
    derive_grants_authority boolean NOT NULL DEFAULT false,
    attached_at             timestamptz NOT NULL DEFAULT now(),
    attached_by             uuid REFERENCES paliad.users(id) ON DELETE SET NULL,
    PRIMARY KEY (project_id, partner_unit_id)
);

CREATE INDEX project_partner_units_unit_idx
    ON paliad.project_partner_units (partner_unit_id, project_id);

ALTER TABLE paliad.project_partner_units ENABLE ROW LEVEL SECURITY;

-- Anyone who can see the project can see the unit attachment. Mirrors the
-- approval_requests / deadlines / appointments policy.
CREATE POLICY project_partner_units_select
    ON paliad.project_partner_units FOR SELECT
    USING (paliad.can_see_project(project_id));

-- Writes gated to global_admin OR project lead. Same pattern as
-- /admin/team and /admin/partner-units precedent.
CREATE POLICY project_partner_units_write
    ON paliad.project_partner_units FOR ALL
    USING (
      EXISTS (SELECT 1 FROM paliad.users u
               WHERE u.id = auth.uid() AND u.global_role = 'global_admin')
      OR EXISTS (SELECT 1 FROM paliad.project_teams pt
                  WHERE pt.user_id = auth.uid()
                    AND pt.project_id = project_partner_units.project_id
                    AND pt.role = 'lead')
    )
    WITH CHECK (
      EXISTS (SELECT 1 FROM paliad.users u
               WHERE u.id = auth.uid() AND u.global_role = 'global_admin')
      OR EXISTS (SELECT 1 FROM paliad.project_teams pt
                  WHERE pt.user_id = auth.uid()
                    AND pt.project_id = project_partner_units.project_id
                    AND pt.role = 'lead')
    );

-- ============================================================================
-- 3. paliad.approval_role_from_unit_role — unit_role → project_role mapping.
--
-- Used when a derived member's authority is evaluated by the t-138 strict
-- ladder. The mapping is intentional:
--   lead       → lead       (the unit's lead, matches project lead tier)
--   attorney   → associate  (default for working lawyers)
--   senior_pa  → senior_pa  (1:1)
--   pa         → pa         (1:1)
--   paralegal  → observer   (level 0 — ineligible to approve)
-- The ApprovalService (t-138) reads project_teams.role first; only when that
-- has no row does it fall back to derived authority via this mapping (and
-- only when the project_partner_units row has derive_grants_authority=true).
-- ============================================================================

CREATE OR REPLACE FUNCTION paliad.approval_role_from_unit_role(unit_role text)
RETURNS text LANGUAGE SQL IMMUTABLE AS $$
    SELECT CASE unit_role
        WHEN 'lead'      THEN 'lead'
        WHEN 'attorney'  THEN 'associate'
        WHEN 'senior_pa' THEN 'senior_pa'
        WHEN 'pa'        THEN 'pa'
        ELSE                  'observer'
    END
$$;

-- ============================================================================
-- 4. Extend paliad.approval_requests.decision_kind CHECK to allow
--    'derived_peer' — a derived (partner-unit) member with authority who
--    signed off via the t-paliad-138 inbox path. Distinct from plain
--    'peer' so the audit trail discloses the derivation chain.
-- ============================================================================

ALTER TABLE paliad.approval_requests DROP CONSTRAINT IF EXISTS approval_requests_decision_kind_check;
ALTER TABLE paliad.approval_requests ADD CONSTRAINT approval_requests_decision_kind_check
    CHECK (decision_kind IS NULL OR decision_kind IN ('peer', 'admin_override', 'derived_peer'));

-- ============================================================================
-- 5. paliad.can_see_project — extended with derivation branch.
--
-- Same shape as the migration-023 body, plus one EXISTS branch: a user is
-- visible on a project if there is any (ancestor of project) attached to a
-- partner_unit they are a member of, AND their unit_role is in the derive
-- set for that attachment. Read-cost is small (project_partner_units +
-- partner_unit_members are tiny).
--
-- t-paliad-139 §3.3 Option B: compute on read, no materialised state.
-- ============================================================================

CREATE OR REPLACE FUNCTION paliad.can_see_project(_project_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = paliad, public
AS $$
    SELECT EXISTS (
        SELECT 1 FROM paliad.users u
         WHERE u.id = auth.uid() AND u.global_role = 'global_admin'
    )
    OR EXISTS (
        SELECT 1
          FROM paliad.projects      target
          JOIN paliad.project_teams pt
            ON pt.user_id = auth.uid()
           AND pt.project_id = ANY(string_to_array(target.path, '.')::uuid[])
         WHERE target.id = _project_id
    )
    OR EXISTS (
        SELECT 1
          FROM paliad.projects                target
          JOIN paliad.project_partner_units   ppu
            ON ppu.project_id = ANY(string_to_array(target.path, '.')::uuid[])
          JOIN paliad.partner_unit_members    pum
            ON pum.partner_unit_id = ppu.partner_unit_id
           AND pum.user_id = auth.uid()
           AND pum.unit_role = ANY(ppu.derive_unit_roles)
         WHERE target.id = _project_id
    );
$$;
