-- t-paliad-160 (M1, slice 1): split approval_policies.required_role into
-- two columns — requires_approval (the gate) + min_role (the seniority
-- threshold). The legacy required_role='none' sentinel conflated two
-- concepts: "approval applies at all" vs "who can approve". This
-- migration introduces the split and backfills.
--
-- M1 = additive + dual-read. New code paths read both old and new columns
-- so a rollback to pre-deploy code keeps working. M2 (follow-up
-- migration) will drop required_role once everything writes the new
-- shape exclusively.
--
-- Resolver semantics also change with this split: when both a project-
-- level row and a partner-unit-default row resolve for the same
-- (entity_type, lifecycle_event), most-strict-wins now applies on BOTH
-- axes:
--   - requires_approval: OR (true if either side says true).
--   - min_role:          MAX along approval_role_level().
-- That update lives in the paliad.approval_policy_effective() rewrite
-- in §4 below.
--
-- Sections:
--   1. ALTER paliad.approval_policies ADD COLUMN requires_approval + min_role.
--   2. Backfill: required_role='none' → (false, NULL); else → (true, role).
--   3. Constraint: (requires_approval=false) OR (min_role IS NOT NULL).
--   4. Replace paliad.approval_policy_effective() with most-strict-wins
--      across the new columns. Returns (requires_approval, min_role,
--      source, source_id) — back-compat shim required_role column kept
--      in result type so callers reading the old column don't break
--      until they cut over.

-- ============================================================================
-- 1. New columns. Both nullable in the schema; the constraint in §3
--    enforces the relationship instead of a NOT NULL on requires_approval
--    (we want Postgres to keep the row out cleanly when min_role is NULL
--    and requires_approval = false).
-- ============================================================================

ALTER TABLE paliad.approval_policies
    ADD COLUMN requires_approval boolean,
    ADD COLUMN min_role          text;

-- ============================================================================
-- 2. Backfill from required_role.
--   'none'     → (false, NULL)
--   else (any of partner/of_counsel/associate/senior_pa/pa) → (true, role)
-- ============================================================================

UPDATE paliad.approval_policies
   SET requires_approval = false,
       min_role          = NULL
 WHERE required_role = 'none';

UPDATE paliad.approval_policies
   SET requires_approval = true,
       min_role          = required_role
 WHERE required_role <> 'none';

-- After backfill every row has a non-NULL requires_approval. Tighten.
ALTER TABLE paliad.approval_policies
    ALTER COLUMN requires_approval SET NOT NULL;

-- ============================================================================
-- 3. The split-grammar invariant: a row that demands approval must name
--    a min_role; a row that does not demand approval has min_role NULL.
-- ============================================================================

ALTER TABLE paliad.approval_policies
    ADD CONSTRAINT approval_policies_min_role_xor_required CHECK (
        (requires_approval = false AND min_role IS NULL)
        OR
        (requires_approval = true  AND min_role IS NOT NULL)
    );

-- min_role values mirror the approval ladder. NULL is allowed (the
-- requires_approval=false branch); any other value must be on the ladder.
ALTER TABLE paliad.approval_policies
    ADD CONSTRAINT approval_policies_min_role_check CHECK (
        min_role IS NULL OR min_role IN (
            'partner', 'of_counsel', 'associate', 'senior_pa', 'pa'
        )
    );

-- ============================================================================
-- 4. paliad.approval_policy_effective — most-strict-wins resolver.
--
-- Returns at most one row, or zero rows when no policy applies. The result
-- shape adds two columns (requires_approval, min_role) while keeping the
-- legacy required_role column for back-compat dual-read. Old callers that
-- still read required_role keep working; new callers branch on
-- requires_approval/min_role.
--
-- Resolution:
--   Step 1 — collect candidates for (project, entity, lifecycle):
--     a) project-specific row (project_id = p_project_id).
--     b) ancestor rows on the project's ltree path (excluding self).
--     c) unit-default rows for partner units attached to this project.
--   Step 2 — most-strict-wins over the union:
--     requires_approval := bool_or(c.requires_approval)  -- true if any says true
--     min_role          := role with MAX(approval_role_level) among the
--                          candidates whose requires_approval=true.
--                          NULL if no candidate demands approval.
--   Step 3 — the project-specific row no longer wins outright. The 'none'
--     sentinel is gone; suppression is now expressed as an explicit
--     requires_approval=false at project level, which loses to any
--     ancestor / unit_default with requires_approval=true under
--     most-strict-wins. This is intentional: the user-locked semantics
--     is "tighten only, never loosen by inheritance" and the project
--     row that wants to relax inherited rules has to be authored at the
--     ancestor / unit level instead. (See t-paliad-160 §A resolver lock.)
--
-- Returned columns:
--   requires_approval boolean  — the gate
--   min_role          text     — the threshold (NULL when gate is off)
--   required_role     text     — back-compat: NULL when gate is off,
--                                else equals min_role. Old callers that
--                                read required_role keep working until
--                                M2 drops the column.
--   source            text     — 'project' | 'ancestor' | 'unit_default'
--                                (the source of the WINNING min_role; for
--                                a pure requires_approval=false result,
--                                the source of the highest-priority
--                                'false' row in the order project >
--                                ancestor > unit_default).
--   source_id         uuid     — project_id or partner_unit_id of source.
-- ============================================================================

DROP FUNCTION IF EXISTS paliad.approval_policy_effective(uuid, text, text);

CREATE FUNCTION paliad.approval_policy_effective(
    p_project_id  uuid,
    p_entity_type text,
    p_lifecycle   text
) RETURNS TABLE (
    requires_approval boolean,
    min_role          text,
    required_role     text,
    source            text,
    source_id         uuid
)
LANGUAGE plpgsql STABLE AS $$
BEGIN
    RETURN QUERY
    WITH path AS (
        SELECT string_to_array(p.path, '.')::uuid[] AS ids
          FROM paliad.projects p WHERE p.id = p_project_id
    ),
    project_rows AS (
        SELECT ap.requires_approval,
               ap.min_role,
               'project'::text AS src,
               ap.project_id   AS sid,
               1               AS src_priority
          FROM paliad.approval_policies ap
         WHERE ap.project_id = p_project_id
           AND ap.entity_type = p_entity_type
           AND ap.lifecycle_event = p_lifecycle
    ),
    ancestor_rows AS (
        SELECT ap.requires_approval,
               ap.min_role,
               'ancestor'::text AS src,
               ap.project_id    AS sid,
               2                AS src_priority
          FROM paliad.approval_policies ap, path
         WHERE ap.project_id = ANY(path.ids)
           AND ap.project_id <> p_project_id
           AND ap.entity_type = p_entity_type
           AND ap.lifecycle_event = p_lifecycle
    ),
    unit_rows AS (
        SELECT ap.requires_approval,
               ap.min_role,
               'unit_default'::text AS src,
               ap.partner_unit_id   AS sid,
               3                    AS src_priority
          FROM paliad.approval_policies ap
          JOIN paliad.project_partner_units ppu
            ON ppu.partner_unit_id = ap.partner_unit_id
         WHERE ppu.project_id = p_project_id
           AND ap.entity_type = p_entity_type
           AND ap.lifecycle_event = p_lifecycle
    ),
    candidates AS (
        SELECT * FROM project_rows
         UNION ALL
        SELECT * FROM ancestor_rows
         UNION ALL
        SELECT * FROM unit_rows
    ),
    -- Pick the strictest min_role: highest approval_role_level among the
    -- requires_approval=true candidates. Tie-break: project > ancestor >
    -- unit_default for stable attribution.
    strictest_role AS (
        SELECT c.min_role,
               c.src AS source,
               c.sid AS source_id
          FROM candidates c
         WHERE c.requires_approval = true
           AND c.min_role IS NOT NULL
         ORDER BY paliad.approval_role_level(c.min_role) DESC,
                  c.src_priority ASC
         LIMIT 1
    ),
    -- If nothing demands approval, surface the project row's "no approval"
    -- if present, else any (false) row with stable tie-break, so attribution
    -- still works for the UI ("inherited from <unit>").
    no_approval_attribution AS (
        SELECT c.src AS source, c.sid AS source_id
          FROM candidates c
         WHERE c.requires_approval = false
         ORDER BY c.src_priority ASC
         LIMIT 1
    ),
    summary AS (
        SELECT bool_or(c.requires_approval) AS req
          FROM candidates c
    )
    SELECT
        COALESCE(s.req, false)                                  AS requires_approval,
        sr.min_role                                             AS min_role,
        sr.min_role                                             AS required_role,
        COALESCE(sr.source, na.source)                          AS source,
        COALESCE(sr.source_id, na.source_id)                    AS source_id
      FROM summary s
      LEFT JOIN strictest_role sr ON true
      LEFT JOIN no_approval_attribution na ON true
     WHERE EXISTS (SELECT 1 FROM candidates);  -- zero rows when no policy applies
END;
$$;

COMMENT ON FUNCTION paliad.approval_policy_effective(uuid, text, text) IS
    'Effective approval policy resolver (t-paliad-160 most-strict-wins). '
    'Returns requires_approval (OR across candidates), min_role (MAX along '
    'the role ladder among requires_approval=true candidates), and the '
    'source attribution. required_role mirrors min_role for back-compat '
    'dual-read with code that hasn''t cut over yet. Zero rows when no '
    'policy candidates exist for the (project, entity_type, lifecycle).';
