-- t-paliad-148: split paliad.project_teams.role into firm-level profession
-- and project-level responsibility.
--
-- Design: docs/design-profession-vs-project-role-2026-05-07.md (kepler,
-- m-locked 2026-05-07 21:35).
--
-- The legacy column did two jobs at once:
--   - career tier at the firm (PA, Associate, Of Counsel, …)
--   - responsibility on this matter (Lead, Member, Observer)
-- This migration introduces two clean axes and backfills both from the
-- legacy column. The legacy column is kept as a deprecated shadow for one
-- release; a follow-up migration drops it after Go code has fully
-- migrated and the production data is verified clean.
--
-- Day-1 deploy = zero behaviour change because the new code paths read
-- the new columns and the backfill is run inside this migration.
--
-- Sections:
--   1. ALTER paliad.users ADD COLUMN profession.
--   2. ALTER paliad.project_teams ADD COLUMN responsibility.
--   3. Backfill profession from highest legacy project_teams.role per user.
--   4. Backfill responsibility from legacy project_teams.role.
--   5. UPDATE paliad.approval_policies.required_role CHECK + 'lead' → 'partner'.
--   6. UPDATE paliad.approval_requests.required_role CHECK + 'lead' → 'partner'.
--   7. UPDATE paliad.approval_role_from_unit_role: lead → partner.
--   8. CREATE paliad.user_project_authority_level — tuple-with-gate ladder.
--   9. CASCADE-rebuild paliad.project_partner_units RLS policies that
--      reference pt.role = 'lead'.
--  10. UPDATE COMMENT on paliad.approval_role_level pointing at users.profession.

-- ============================================================================
-- 1. paliad.users.profession — firm-wide career tier.
--
-- NULL means "no firm tier" (external local counsel, expert, admin
-- accounts that aren't practicing lawyers). NULL → ladder level 0 →
-- ineligible to approve. Required-on-invite for firm members; admin
-- editable on /admin/team.
-- ============================================================================

ALTER TABLE paliad.users
    ADD COLUMN profession text NULL
        CHECK (profession IS NULL OR profession IN (
            'partner', 'of_counsel', 'associate',
            'senior_pa', 'pa', 'paralegal'
        ));

CREATE INDEX users_profession_idx ON paliad.users (profession);

COMMENT ON COLUMN paliad.users.profession IS
    'Firm-wide career tier driving the t-paliad-138 approval ladder. '
    'NULL = no firm tier (external collaborators, admin accounts). '
    'Distinct from job_title (free-text display) and global_role (tool admin gate).';

-- ============================================================================
-- 2. paliad.project_teams.responsibility — per-project responsibility.
--
-- Replaces the project-axis values that were mixed into project_teams.role.
-- Default 'member'. 'lead' has additional manage-project privileges (already
-- wired in derivation_service.go). 'observer' and 'external' close the
-- approval gate (level 0 regardless of profession).
--
-- The legacy `role` column is kept on the table as a deprecated shadow for
-- one release. New code reads .responsibility; old code paths that still
-- read .role continue working until the follow-up migration drops the
-- column.
-- ============================================================================

ALTER TABLE paliad.project_teams
    ADD COLUMN responsibility text NOT NULL DEFAULT 'member'
        CHECK (responsibility IN ('lead', 'member', 'observer', 'external'));

CREATE INDEX project_teams_responsibility_idx
    ON paliad.project_teams (project_id, responsibility);

COMMENT ON COLUMN paliad.project_teams.responsibility IS
    'Per-project responsibility on this matter. lead/member open the '
    'approval gate; observer/external close it. Profession provides the '
    'level (paliad.users.profession).';

COMMENT ON COLUMN paliad.project_teams.role IS
    'DEPRECATED — split into users.profession + project_teams.responsibility '
    'in migration 057 (t-paliad-148). Kept as a shadow column for one release. '
    'Drop in follow-up migration 058.';

-- ============================================================================
-- 3. Backfill paliad.users.profession from highest legacy tier per user.
--
-- Mapping (legacy role → profession):
--   lead         → partner
--   of_counsel   → of_counsel
--   associate    → associate
--   senior_pa    → senior_pa
--   pa           → pa
--   local_counsel/expert/observer → IGNORED (no firm tier inferable)
--
-- For each user with at least one project_teams row carrying a firm-tier
-- value, take the HIGHEST tier (per the t-138 ladder). Ties at same tier
-- collapse trivially (same value). Users with only project-only labels
-- (observer / local_counsel / expert) get profession=NULL — admin will
-- need to fill them in via /admin/team.
-- ============================================================================

WITH legacy_to_profession AS (
    SELECT pt.user_id,
           CASE pt.role
               WHEN 'lead'       THEN 'partner'
               WHEN 'of_counsel' THEN 'of_counsel'
               WHEN 'associate'  THEN 'associate'
               WHEN 'senior_pa'  THEN 'senior_pa'
               WHEN 'pa'         THEN 'pa'
               -- observer / local_counsel / expert → NULL (filtered below)
           END AS profession,
           CASE pt.role
               WHEN 'lead'       THEN 5
               WHEN 'of_counsel' THEN 4
               WHEN 'associate'  THEN 3
               WHEN 'senior_pa'  THEN 2
               WHEN 'pa'         THEN 1
               ELSE 0
           END AS lvl
      FROM paliad.project_teams pt
),
ranked AS (
    SELECT user_id, profession,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY lvl DESC) AS rn
      FROM legacy_to_profession
     WHERE profession IS NOT NULL
)
UPDATE paliad.users u
   SET profession = r.profession
  FROM ranked r
 WHERE u.id = r.user_id
   AND r.rn = 1
   AND u.profession IS NULL;

-- ============================================================================
-- 4. Backfill paliad.project_teams.responsibility from legacy role.
--
-- Per-row mapping:
--   lead          → lead
--   observer      → observer
--   local_counsel → external
--   expert        → external
--   associate / pa / of_counsel / senior_pa → member
--
-- Authority for "member" rows now comes from the user's profession.
-- ============================================================================

UPDATE paliad.project_teams
   SET responsibility = CASE role
       WHEN 'lead'          THEN 'lead'
       WHEN 'observer'      THEN 'observer'
       WHEN 'local_counsel' THEN 'external'
       WHEN 'expert'        THEN 'external'
       ELSE                       'member'
   END;

-- ============================================================================
-- 5. paliad.approval_policies.required_role — drop 'lead', add 'partner'.
--
-- Legacy 'lead' was the project-level value at the ladder ceiling; under the
-- new model the ceiling is profession='partner'. Backfill any existing
-- policy rows from 'lead' to 'partner', then tighten the CHECK.
-- ============================================================================

UPDATE paliad.approval_policies
   SET required_role = 'partner'
 WHERE required_role = 'lead';

ALTER TABLE paliad.approval_policies DROP CONSTRAINT IF EXISTS approval_policies_required_role_check;
ALTER TABLE paliad.approval_policies ADD CONSTRAINT approval_policies_required_role_check
    CHECK (required_role IN ('partner', 'of_counsel', 'associate', 'senior_pa', 'pa'));

-- ============================================================================
-- 6. paliad.approval_requests.required_role — same rename for snapshots.
--
-- Each request snapshots the policy's required_role at submission time so
-- mid-flight policy edits don't change the bar. Backfill 'lead' → 'partner'
-- for parity with the new policy enum, then tighten the CHECK.
-- ============================================================================

UPDATE paliad.approval_requests
   SET required_role = 'partner'
 WHERE required_role = 'lead';

ALTER TABLE paliad.approval_requests DROP CONSTRAINT IF EXISTS approval_requests_required_role_check;
ALTER TABLE paliad.approval_requests ADD CONSTRAINT approval_requests_required_role_check
    CHECK (required_role IN ('partner', 'of_counsel', 'associate', 'senior_pa', 'pa'));

-- ============================================================================
-- 7. paliad.approval_role_from_unit_role — bridge maps lead → partner now.
--
-- Derived authority via partner-unit attachments (t-paliad-139) bridges
-- unit_role to the project-tier ladder. Under the new ladder, the highest
-- tier is 'partner' (was 'lead'). Update the lead → lead row to lead →
-- partner; the rest of the bridge mapping stays unchanged.
-- ============================================================================

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 'partner'
        WHEN 'attorney'  THEN 'associate'
        WHEN 'senior_pa' THEN 'senior_pa'
        WHEN 'pa'        THEN 'pa'
        ELSE                  'observer'
    END
$$;

-- Update the level helper too: 'partner' replaces 'lead' as the ceiling
-- value that the function recognises. The numeric ladder is identical;
-- only the named tier shifts.

CREATE OR REPLACE FUNCTION paliad.approval_role_level(role text)
RETURNS int LANGUAGE SQL IMMUTABLE AS $$
    SELECT CASE role
        WHEN 'partner'    THEN 5
        WHEN 'of_counsel' THEN 4
        WHEN 'associate'  THEN 3
        WHEN 'senior_pa'  THEN 2
        WHEN 'pa'         THEN 1
        WHEN 'paralegal'  THEN 0
        -- Legacy 'lead' kept at level 5 for the deprecated-shadow window:
        -- old call sites that still read pt.role would otherwise return
        -- level 0 and break authority for projects where the migration
        -- has run but the Go redirect hasn't. Removed in migration 058.
        WHEN 'lead'       THEN 5
        ELSE 0
    END
$$;

COMMENT ON FUNCTION paliad.approval_role_level(text) IS
    'Strict-ladder level for the t-paliad-138 / t-paliad-148 approval gate. '
    'Reads paliad.users.profession; legacy project_teams.role values still '
    'recognised via the lead→5 shadow row until migration 058 retires the '
    'column. Higher level always satisfies lower; level 0 = ineligible.';

-- ============================================================================
-- 8. paliad.user_project_authority_level — tuple-with-gate ladder.
--
-- effective_level for user U on project P:
--
--   profession_level  = approval_role_level(U.profession)        -- 0 if NULL
--   responsibility    = direct or ancestor on project P
--   gate_open         = responsibility IN ('lead', 'member')
--   derived_role      = approval_role_from_unit_role(unit_role)
--                       when project_partner_units.derive_grants_authority
--   effective_level   = MAX over sources, gated as above
--
-- Direct/ancestor responsibility opens the gate, profession provides the
-- level. Derivation is its own source — derived authority always opens
-- its own gate (the unit attachment's grants_authority flag is the gate).
-- A user can hit this function via direct membership AND derivation; the
-- result is the max of both sources.
-- ============================================================================

CREATE OR REPLACE FUNCTION paliad.user_project_authority_level(
    _user_id uuid,
    _project_id uuid
) RETURNS int
LANGUAGE sql
STABLE
AS $$
    WITH path AS (
        SELECT string_to_array(p.path, '.')::uuid[] AS ids
          FROM paliad.projects p WHERE p.id = _project_id
    ),
    direct_or_ancestor AS (
        SELECT pt.responsibility
          FROM paliad.project_teams pt
          JOIN path ON pt.project_id = ANY(path.ids)
         WHERE pt.user_id = _user_id
    ),
    profession_level AS (
        SELECT paliad.approval_role_level(u.profession) AS lvl
          FROM paliad.users u WHERE u.id = _user_id
    ),
    direct_level AS (
        -- Profession-level if any membership row opens the gate, else 0.
        SELECT CASE
            WHEN EXISTS (
                SELECT 1 FROM direct_or_ancestor doa
                 WHERE doa.responsibility IN ('lead', 'member')
            ) THEN COALESCE((SELECT lvl FROM profession_level), 0)
            ELSE 0
        END AS lvl
    ),
    derived_level AS (
        SELECT COALESCE(MAX(paliad.approval_role_level(
                   paliad.approval_role_from_unit_role(pum.unit_role)
               )), 0) AS lvl
          FROM paliad.project_partner_units ppu
          JOIN paliad.partner_unit_members pum
            ON pum.partner_unit_id = ppu.partner_unit_id
           AND pum.user_id = _user_id
           AND pum.unit_role = ANY(ppu.derive_unit_roles)
          JOIN path ON ppu.project_id = ANY(path.ids)
         WHERE ppu.derive_grants_authority = true
    )
    SELECT GREATEST(
        (SELECT lvl FROM direct_level),
        (SELECT lvl FROM derived_level)
    );
$$;

COMMENT ON FUNCTION paliad.user_project_authority_level(uuid, uuid) IS
    'Effective approval-ladder level for user U on project P, evaluated as '
    'a tuple-with-gate: profession_level if responsibility ∈ {lead,member} '
    'else 0; max with derived authority (partner-unit attachment with '
    'grants_authority=true). t-paliad-148.';

-- ============================================================================
-- 9. paliad.project_partner_units RLS — switch lead-gate to .responsibility.
--
-- Migration 055 wrote two policies that gate writes on pt.role = 'lead'.
-- Under the new model, lead is a project responsibility, not a profession.
-- Drop and rewrite both policies to read .responsibility.
-- ============================================================================

DROP POLICY IF EXISTS project_partner_units_write ON paliad.project_partner_units;

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.responsibility = '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.responsibility = 'lead')
    );
