-- t-paliad-138: dual-control approvals (4-Augen-Prüfung) for Deadlines + Appointments.
--
-- Design: docs/design-approvals-2026-05-06.md (cronus, m-locked 2026-05-06).
--
-- Schema-only migration (commit 1 of 8). Adds the operational tables, the
-- strict-ladder helper, and the per-entity tracking columns. No Go code
-- reads these yet — paliad behaves identically until commit 2 wires the
-- ApprovalService into the mutation paths.
--
-- Sections:
--   1. Add 'senior_pa' to paliad.project_teams.role CHECK.
--   2. paliad.approval_role_level(text) — strict ladder helper.
--   3. paliad.approval_policies — per-(project, entity_type, lifecycle_event).
--   4. paliad.approval_requests — operational pending workflow.
--   5. ALTER paliad.deadlines + paliad.appointments — approval columns
--      (approval_status, pending_request_id, approved_by, approved_at;
--      appointments also gains completed_at).
--   6. Backfill: mark every existing row approval_status='legacy'.
--
-- ============================================================================
-- 1. Add 'senior_pa' to paliad.project_teams.role CHECK.
--
-- Live-DB finding (cronus, 2026-05-06): the existing constraint is named
-- `projekt_teams_role_check` (German leftover from migration 018, when the
-- table was `paliad.projekt_teams`; the table was renamed in 020 but the
-- constraint name was preserved by Postgres). Dropping by both names
-- defensively handles any future re-creation under the English name.
-- ============================================================================

ALTER TABLE paliad.project_teams DROP CONSTRAINT IF EXISTS projekt_teams_role_check;
ALTER TABLE paliad.project_teams DROP CONSTRAINT IF EXISTS project_teams_role_check;
ALTER TABLE paliad.project_teams ADD CONSTRAINT project_teams_role_check
    CHECK (role IN (
        'lead', 'associate', 'pa', 'of_counsel',
        'local_counsel', 'expert', 'observer',
        'senior_pa'
    ));

-- ============================================================================
-- 2. paliad.approval_role_level — strict ladder over project_teams.role.
--
-- Mirrors internal/services/approval_levels.go:levelOf. A user with
-- project_teams.role R can approve any request whose required_role has level
-- <= level(R). Roles outside the approval ladder (local_counsel, expert,
-- observer, anything new) return 0 and are ineligible to approve at any
-- level. Default required_role on policies is 'associate' (level 3).
-- ============================================================================

CREATE OR REPLACE FUNCTION paliad.approval_role_level(role text)
RETURNS int LANGUAGE SQL IMMUTABLE AS $$
    SELECT CASE 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
$$;

COMMENT ON FUNCTION paliad.approval_role_level(text) IS
    'Strict-ladder level for approval gating (t-paliad-138). '
    'Higher level always satisfies lower. Level 0 = ineligible. '
    'Default policy required_role=associate (level 3) — eligible: lead, of_counsel, associate.';

-- ============================================================================
-- 3. paliad.approval_policies — per-(project, entity_type, lifecycle_event).
--
-- Up to 8 rows per project (deadline×4 + appointment×4). UNIQUE composite key
-- enforces this. No row = no approval needed for that event. Authoring is
-- gated to global_admin in the service layer; RLS lets project members read
-- their own project's policies (transparency: "do my edits need 4-eye?").
-- ============================================================================

CREATE TABLE paliad.approval_policies (
    id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id      uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE,
    entity_type     text NOT NULL CHECK (entity_type IN ('deadline', 'appointment')),
    lifecycle_event text NOT NULL CHECK (lifecycle_event IN ('create', 'update', 'complete', 'delete')),
    required_role   text NOT NULL CHECK (required_role IN (
                        'lead', 'of_counsel', 'associate', 'senior_pa', 'pa'
                    )),
    created_at      timestamptz NOT NULL DEFAULT now(),
    updated_at      timestamptz NOT NULL DEFAULT now(),
    created_by      uuid REFERENCES paliad.users(id) ON DELETE SET NULL,
    UNIQUE (project_id, entity_type, lifecycle_event)
);

CREATE INDEX approval_policies_project_idx
    ON paliad.approval_policies (project_id);

ALTER TABLE paliad.approval_policies ENABLE ROW LEVEL SECURITY;

CREATE POLICY approval_policies_select ON paliad.approval_policies
    FOR SELECT TO authenticated
    USING (paliad.can_see_project(project_id));

-- Writes are restricted to global_admin in the application layer. The
-- service-role connection bypasses RLS, so these policies are
-- defence-in-depth for any future direct-DB access path.
CREATE POLICY approval_policies_write ON paliad.approval_policies
    FOR ALL TO authenticated
    USING (
        EXISTS (SELECT 1 FROM paliad.users u
                 WHERE u.id = auth.uid() AND u.global_role = 'global_admin')
    )
    WITH CHECK (
        EXISTS (SELECT 1 FROM paliad.users u
                 WHERE u.id = auth.uid() AND u.global_role = 'global_admin')
    );

-- ============================================================================
-- 4. paliad.approval_requests — operational pending workflow.
--
-- One row per submitted state-change that needs 4-eye sign-off. The entity
-- being changed is referenced by (entity_type, entity_id) — polymorphic
-- across deadlines / appointments, so no FK constraint on entity_id.
--
-- pre_image carries the field values needed to revert on rejection
-- (NULL for 'create' since there's nothing to revert to). payload echoes
-- the diff or new values that were written, for audit display.
--
-- required_role is a snapshot of the policy at request time — even if the
-- policy changes mid-flight, the request honours the level it was submitted
-- under.
--
-- decision_kind discriminates 'peer' (normal in-team sign-off) from
-- 'admin_override' (global_admin used the escape-hatch path). Verlauf
-- chronology renders these distinctly.
--
-- The CHECK on (decided_by != requested_by) is defence-in-depth alongside
-- the service-layer self-approval block.
-- ============================================================================

CREATE TABLE paliad.approval_requests (
    id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id      uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE,
    entity_type     text NOT NULL CHECK (entity_type IN ('deadline', 'appointment')),
    entity_id       uuid NOT NULL,
    lifecycle_event text NOT NULL CHECK (lifecycle_event IN ('create', 'update', 'complete', 'delete')),
    pre_image       jsonb,
    payload         jsonb,
    requested_by    uuid NOT NULL REFERENCES paliad.users(id) ON DELETE RESTRICT,
    requested_at    timestamptz NOT NULL DEFAULT now(),
    required_role   text NOT NULL CHECK (required_role IN (
                        'lead', 'of_counsel', 'associate', 'senior_pa', 'pa'
                    )),
    status          text NOT NULL DEFAULT 'pending'
                    CHECK (status IN ('pending', 'approved', 'rejected', 'revoked', 'superseded')),
    decided_by      uuid REFERENCES paliad.users(id) ON DELETE SET NULL,
    decided_at      timestamptz,
    decision_kind   text CHECK (decision_kind IS NULL OR decision_kind IN ('peer', 'admin_override')),
    decision_note   text,
    created_at      timestamptz NOT NULL DEFAULT now(),
    updated_at      timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT approval_requests_no_self_approval
        CHECK (decided_by IS NULL OR decided_by <> requested_by)
);

CREATE INDEX approval_requests_project_status_idx
    ON paliad.approval_requests (project_id, status);
CREATE INDEX approval_requests_entity_idx
    ON paliad.approval_requests (entity_type, entity_id);
CREATE INDEX approval_requests_requested_by_idx
    ON paliad.approval_requests (requested_by, status);
CREATE INDEX approval_requests_pending_idx
    ON paliad.approval_requests (status, requested_at)
    WHERE status = 'pending';

ALTER TABLE paliad.approval_requests ENABLE ROW LEVEL SECURITY;

-- Visible to anyone with project visibility (mirrors deadlines / appointments).
-- The approve/reject action is gated at the service layer, not here.
CREATE POLICY approval_requests_all ON paliad.approval_requests
    FOR ALL TO authenticated
    USING (paliad.can_see_project(project_id))
    WITH CHECK (paliad.can_see_project(project_id));

-- ============================================================================
-- 5. Approval columns on paliad.deadlines + paliad.appointments.
--
-- approval_status:
--   'approved' (default for new + existing-after-backfill-clears),
--   'pending'  (an approval_request is in flight; pending_request_id set),
--   'legacy'   (predates 4-eye; backfilled in §6 below).
--
-- pending_request_id: FK to the in-flight approval_requests row. NULL when
-- approval_status != 'pending'. ON DELETE SET NULL keeps the entity row
-- intact if an approval_requests row is ever pruned.
--
-- approved_by / approved_at: set on transition to approval_status='approved'
-- after a 4-eye approval. NULL for 'legacy' rows and rows that never went
-- through 4-eye (no policy applied).
--
-- appointments.completed_at: new column for the appointment:complete
-- lifecycle event. Nullable; NULL means "not yet marked done".
-- ============================================================================

ALTER TABLE paliad.deadlines
    ADD COLUMN approval_status    text NOT NULL DEFAULT 'approved'
        CHECK (approval_status IN ('approved', 'pending', 'legacy')),
    ADD COLUMN pending_request_id uuid
        REFERENCES paliad.approval_requests(id) ON DELETE SET NULL,
    ADD COLUMN approved_by        uuid REFERENCES paliad.users(id) ON DELETE SET NULL,
    ADD COLUMN approved_at        timestamptz;

CREATE INDEX deadlines_approval_status_pending_idx
    ON paliad.deadlines (approval_status)
    WHERE approval_status = 'pending';

ALTER TABLE paliad.appointments
    ADD COLUMN approval_status    text NOT NULL DEFAULT 'approved'
        CHECK (approval_status IN ('approved', 'pending', 'legacy')),
    ADD COLUMN pending_request_id uuid
        REFERENCES paliad.approval_requests(id) ON DELETE SET NULL,
    ADD COLUMN approved_by        uuid REFERENCES paliad.users(id) ON DELETE SET NULL,
    ADD COLUMN approved_at        timestamptz,
    ADD COLUMN completed_at       timestamptz;

CREATE INDEX appointments_approval_status_pending_idx
    ON paliad.appointments (approval_status)
    WHERE approval_status = 'pending';

-- ============================================================================
-- 6. Backfill: mark every existing row legacy.
--
-- Per design §6.5 / m's Q11 answer: existing pre-4-eye rows are read-clean;
-- they don't need retroactive approval. The next mutation on a legacy row
-- that hits an active policy (none exist on day 1) will trigger normal flow
-- and lift the row to 'approved' (or 'pending' until signed off).
--
-- created_by is already populated since migration 005. approved_by stays
-- NULL on legacy rows.
-- ============================================================================

UPDATE paliad.deadlines    SET approval_status = 'legacy';
UPDATE paliad.appointments SET approval_status = 'legacy';
