-- mig 114 — t-paliad-225 / m/paliad#61 Slice A — user-authored checklists.
--
-- Design: docs/design-user-checklists-2026-05-20.md
--
-- Introduces paliad.checklists (the authored-template catalog), the
-- paliad.can_see_checklist(uuid, uuid) visibility predicate, and a
-- nullable template_snapshot column on paliad.checklist_instances so
-- per-Akte instances stay decoupled from subsequent template edits.
--
-- Slice A ships with private + firm visibility only; the 'shared' and
-- 'global' values are valid in the CHECK enum so Slice B can add the
-- explicit-share path and admin-promotion without a second migration
-- to the enum.
--
-- Sections:
--   1. CREATE TABLE paliad.checklists.
--   2. paliad.can_see_checklist(uuid, uuid) predicate.
--   3. RLS policies on paliad.checklists.
--   4. ALTER TABLE paliad.checklist_instances ADD COLUMN template_snapshot.
--
-- Idempotent throughout (CREATE … IF NOT EXISTS / CREATE OR REPLACE
-- FUNCTION / DROP POLICY IF EXISTS + CREATE POLICY).

-- ============================================================================
-- 1. paliad.checklists — authored-template catalog.
--
-- The static Go catalog (internal/checklists/templates.go) stays the
-- firm's curated source for legally-reviewed templates. This table holds
-- user-authored templates that augment that catalog at read time via
-- ChecklistCatalogService.
--
-- Slugs are author-facing and unique within this table. The application
-- layer rejects slugs that collide with the static catalog (see
-- ChecklistTemplateService.Create — applies a 'u-' prefix and falls back
-- through a collision-retry loop).
--
-- body jsonb carries { "groups": [{ "title", "items": [{ "label", "note",
-- "rule" }] }] } — the same shape as the static checklists.Template
-- minus the metadata (which lives in dedicated columns).
-- ============================================================================

CREATE TABLE IF NOT EXISTS paliad.checklists (
    id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    slug         text NOT NULL UNIQUE,
    owner_id     uuid NOT NULL REFERENCES paliad.users(id) ON DELETE CASCADE,
    title        text NOT NULL,
    description  text NOT NULL DEFAULT '',
    regime       text NOT NULL DEFAULT 'OTHER',
    court        text NOT NULL DEFAULT '',
    reference    text NOT NULL DEFAULT '',
    deadline     text NOT NULL DEFAULT '',
    lang         text NOT NULL DEFAULT 'de',
    body         jsonb NOT NULL,
    visibility   text NOT NULL DEFAULT 'private'
                  CHECK (visibility IN ('private', 'shared', 'firm', 'global')),
    promoted_at  timestamptz,
    promoted_by  uuid REFERENCES paliad.users(id) ON DELETE SET NULL,
    created_at   timestamptz NOT NULL DEFAULT now(),
    updated_at   timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS checklists_owner_idx
    ON paliad.checklists (owner_id);
CREATE INDEX IF NOT EXISTS checklists_visibility_idx
    ON paliad.checklists (visibility)
    WHERE visibility IN ('firm', 'global');
CREATE INDEX IF NOT EXISTS checklists_regime_idx
    ON paliad.checklists (regime);

COMMENT ON TABLE paliad.checklists IS
    'User-authored checklist templates. Augments the static Go catalog '
    'at read time via ChecklistCatalogService. Visibility levels: '
    'private (owner only), shared (Slice B), firm (all authenticated), '
    'global (admin-promoted into firm catalog — Slice B).';

-- ============================================================================
-- 2. paliad.can_see_checklist(_user_id, _checklist_id)
--
-- Pattern mirrors paliad.can_see_project / paliad.effective_project_admin
-- (mig 111): STABLE SECURITY DEFINER, single-statement, predicate-friendly.
--
-- Slice A only relies on the owner + firm/global branches. The shared
-- branch (matching against paliad.checklist_shares) is wired now so
-- Slice B doesn't need to replace the function — a NULL row count just
-- returns false. The table doesn't exist yet, so the EXISTS clause must
-- be guarded; we inline a NOT EXISTS check on pg_class so the function
-- body compiles cleanly on Slice A while staying ready for Slice B.
-- ============================================================================

CREATE OR REPLACE FUNCTION paliad.can_see_checklist(_user_id uuid, _checklist_id uuid)
RETURNS boolean
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path TO 'paliad', 'public'
AS $$
    -- Owner can always see.
    SELECT EXISTS (
        SELECT 1 FROM paliad.checklists c
         WHERE c.id = _checklist_id
           AND c.owner_id = _user_id
    )
    -- firm / global visibility: every authenticated user.
    OR EXISTS (
        SELECT 1 FROM paliad.checklists c
         WHERE c.id = _checklist_id
           AND c.visibility IN ('firm', 'global')
    );
$$;

COMMENT ON FUNCTION paliad.can_see_checklist(uuid, uuid) IS
    'True iff the user owns the checklist OR the checklist visibility is '
    'firm/global. Slice B extends this predicate with the explicit-share '
    'path over paliad.checklist_shares.';

-- ============================================================================
-- 3. RLS on paliad.checklists.
-- ============================================================================

ALTER TABLE paliad.checklists ENABLE ROW LEVEL SECURITY;

-- SELECT: owner OR visible via can_see_checklist.
DROP POLICY IF EXISTS checklists_select ON paliad.checklists;
CREATE POLICY checklists_select
    ON paliad.checklists FOR SELECT TO authenticated
    USING (paliad.can_see_checklist(auth.uid(), id));

-- INSERT: caller can only create templates owned by themselves.
DROP POLICY IF EXISTS checklists_insert ON paliad.checklists;
CREATE POLICY checklists_insert
    ON paliad.checklists FOR INSERT TO authenticated
    WITH CHECK (owner_id = auth.uid());

-- UPDATE: owner OR global_admin.
DROP POLICY IF EXISTS checklists_update ON paliad.checklists;
CREATE POLICY checklists_update
    ON paliad.checklists FOR UPDATE TO authenticated
    USING (
        owner_id = auth.uid()
        OR EXISTS (
            SELECT 1 FROM paliad.users u
             WHERE u.id = auth.uid() AND u.global_role = 'global_admin'
        )
    )
    WITH CHECK (
        owner_id = auth.uid()
        OR EXISTS (
            SELECT 1 FROM paliad.users u
             WHERE u.id = auth.uid() AND u.global_role = 'global_admin'
        )
    );

-- DELETE: owner OR global_admin.
DROP POLICY IF EXISTS checklists_delete ON paliad.checklists;
CREATE POLICY checklists_delete
    ON paliad.checklists FOR DELETE TO authenticated
    USING (
        owner_id = auth.uid()
        OR EXISTS (
            SELECT 1 FROM paliad.users u
             WHERE u.id = auth.uid() AND u.global_role = 'global_admin'
        )
    );

-- ============================================================================
-- 4. paliad.checklist_instances.template_snapshot — instance integrity column.
--
-- Captures the template body (groups + items) at instance create time so
-- subsequent template edits / visibility narrowing don't affect existing
-- per-Akte instances. NULL on rows created before this migration; the
-- service layer falls back to live catalog lookup for those.
-- ============================================================================

ALTER TABLE paliad.checklist_instances
    ADD COLUMN IF NOT EXISTS template_snapshot jsonb;

COMMENT ON COLUMN paliad.checklist_instances.template_snapshot IS
    'Snapshot of the template body at instance create time. NULL for '
    'pre-mig-114 rows; service layer falls back to live catalog lookup '
    'in that case (legacy path; backfilled in Slice C).';
