-- 151_dedupe_null_procedural_events — t-paliad-319 / m/paliad#144
--
-- Purpose: ~14 paliad.procedural_events rows with synthetic null.<8hex>
-- codes (minted by mig 136 from the legacy paliad.deadline_rules rows
-- whose submission_code was NULL) share user-visible names. The
-- /admin/procedural-events list shows multiple entries for the same legal
-- concept (worst offender: "Mängelbeseitigung / Zahlung" × 6). This
-- migration consolidates every name-group onto a single canonical row,
-- reparents the sequencing_rules pointing at the duplicates, and archives
-- the duplicates without deleting them.
--
-- Scope verified live before write (Supabase MCP, 2026-05-26):
--   * 5 name-groups, 14 duplicate rows total (1 canonical + 1–5 dups per
--     group). Every duplicate has exactly 1 sequencing_rule pointing at it.
--   * 0 paliad.deadlines reference any duplicate.
--   * 0 procedural_events.draft_of references any duplicate.
--   * No audit trigger on procedural_events or sequencing_rules — only
--     the INSTEAD OF triggers on deadline_rules_unified (mig 140), which
--     do not fire on direct table writes. No set_config('paliad.audit_reason')
--     needed.
--
-- Canonical selection: ROW_NUMBER() OVER (PARTITION BY name ORDER BY
-- created_at, id::text). Every duplicate in current data shares the same
-- created_at (mig 136 bulk insert), so the deterministic tiebreaker is
-- the UUID's lexicographic order.
--
-- Hard constraints honoured:
--   * No deletions. Duplicates flip to is_active=false +
--     lifecycle_state='archived'. The rows stay in the table for audit.
--   * Reparent sequencing_rules.procedural_event_id duplicate → canonical
--     BEFORE archiving, so no FK ever points at an archived PE.
--   * Snapshot the affected procedural_events + sequencing_rules into
--     paliad.procedural_events_pre_151 / paliad.sequencing_rules_pre_151
--     in the same TX, mirroring precedent (migs 091/093/095/098/140).
--
-- Down: best-effort restore from the snapshots. See .down.sql.

-- ----------------------------------------------------------------
-- 1. Build the dedupe mapping (duplicate_id → canonical_id) in a
--    TEMP table used by every subsequent step.
-- ----------------------------------------------------------------

CREATE TEMP TABLE tmp_pe_dedupe ON COMMIT DROP AS
WITH dupe_names AS (
    SELECT name
      FROM paliad.procedural_events
     WHERE code LIKE 'null.%'
     GROUP BY name
    HAVING COUNT(*) > 1
),
ranked AS (
    SELECT pe.id,
           pe.code,
           pe.name,
           pe.created_at,
           ROW_NUMBER() OVER (
               PARTITION BY pe.name
               ORDER BY pe.created_at, pe.id::text
           ) AS rn
      FROM paliad.procedural_events pe
     WHERE pe.code LIKE 'null.%'
       AND pe.name IN (SELECT name FROM dupe_names)
),
canonicals AS (
    SELECT name,
           id   AS canonical_id,
           code AS canonical_code
      FROM ranked
     WHERE rn = 1
)
SELECT r.id   AS duplicate_id,
       r.code AS duplicate_code,
       r.name,
       c.canonical_id,
       c.canonical_code
  FROM ranked r
  JOIN canonicals c ON c.name = r.name
 WHERE r.rn > 1;

-- ----------------------------------------------------------------
-- 2. Snapshot. Captures the rows that change so .down has a clean
--    source of truth; mirrors the pre_091/093/095/098/140 precedent.
-- ----------------------------------------------------------------

CREATE TABLE paliad.procedural_events_pre_151 AS
SELECT pe.*
  FROM paliad.procedural_events pe
 WHERE pe.id IN (SELECT duplicate_id FROM tmp_pe_dedupe);

COMMENT ON TABLE paliad.procedural_events_pre_151 IS
    'Snapshot (mig 151, t-paliad-319) of the null.* procedural_events '
    'duplicates that were archived in favour of their canonical name-mate. '
    'Read-only forensic + revert source. Mirrors precedent pre_091/093/'
    '095/098/140.';

CREATE TABLE paliad.sequencing_rules_pre_151 AS
SELECT sr.id,
       sr.procedural_event_id AS original_procedural_event_id
  FROM paliad.sequencing_rules sr
 WHERE sr.procedural_event_id IN (SELECT duplicate_id FROM tmp_pe_dedupe);

COMMENT ON TABLE paliad.sequencing_rules_pre_151 IS
    'Snapshot (mig 151, t-paliad-319) of sequencing_rules.procedural_event_id '
    'before reparenting from null.* duplicates onto their canonical PE. '
    'Read-only forensic + revert source.';

-- ----------------------------------------------------------------
-- 3. Audit log — per-row NOTICE so the migration output captures
--    exactly which duplicate folded into which canonical, including
--    the sr_count for the duplicate (always 1 in current data, but
--    the RAISE keeps the audit honest if the scope grows later).
-- ----------------------------------------------------------------

DO $$
DECLARE
    rec record;
    v_dup_count int;
    v_grp_count int;
BEGIN
    SELECT COUNT(*), COUNT(DISTINCT name)
      INTO v_dup_count, v_grp_count
      FROM tmp_pe_dedupe;

    RAISE NOTICE '[mig 151] dedupe scope: % duplicate rows across % name-groups',
        v_dup_count, v_grp_count;

    FOR rec IN
        SELECT d.duplicate_id,
               d.duplicate_code,
               d.name,
               d.canonical_id,
               d.canonical_code,
               (SELECT COUNT(*)
                  FROM paliad.sequencing_rules sr
                 WHERE sr.procedural_event_id = d.duplicate_id) AS sr_count
          FROM tmp_pe_dedupe d
         ORDER BY d.name, d.duplicate_id
    LOOP
        RAISE NOTICE '[mig 151]   dup % (%) -> canonical % (%) — sr_count=%',
            rec.duplicate_id, rec.duplicate_code,
            rec.canonical_id, rec.canonical_code,
            rec.sr_count;
        RAISE NOTICE '[mig 151]     name: %', rec.name;
    END LOOP;
END $$;

-- ----------------------------------------------------------------
-- 4. Reparent sequencing_rules.procedural_event_id duplicate → canonical.
--    sequencing_rules_pe_proc_lifecycle_idx is non-unique, so collapsing
--    multiple sr onto one PE is by design.
-- ----------------------------------------------------------------

UPDATE paliad.sequencing_rules sr
   SET procedural_event_id = d.canonical_id,
       updated_at          = now()
  FROM tmp_pe_dedupe d
 WHERE sr.procedural_event_id = d.duplicate_id;

-- ----------------------------------------------------------------
-- 5. Archive the duplicates. No deletion — audit trail preserved.
-- ----------------------------------------------------------------

UPDATE paliad.procedural_events pe
   SET is_active       = false,
       lifecycle_state = 'archived',
       updated_at      = now()
 WHERE pe.id IN (SELECT duplicate_id FROM tmp_pe_dedupe);

-- ----------------------------------------------------------------
-- 6. POST assertions. Any failure rolls the migration back.
-- ----------------------------------------------------------------

DO $$
DECLARE
    v_surviving_groups int;
    v_expected_count   int;
    v_archived_count   int;
    v_orphan_sr        int;
BEGIN
    -- (a) Acceptance criterion 2: no name-group still has >1 active+
    --     published null.* row.
    SELECT COUNT(*) INTO v_surviving_groups
      FROM (
        SELECT name
          FROM paliad.procedural_events
         WHERE code LIKE 'null.%'
           AND is_active = true
           AND lifecycle_state = 'published'
         GROUP BY name
        HAVING COUNT(*) > 1
      ) s;

    IF v_surviving_groups > 0 THEN
        RAISE EXCEPTION
            '[mig 151] FAILED POST: % name-groups still have >1 active+published null.* rows',
            v_surviving_groups;
    END IF;

    -- (b) Every targeted duplicate is now archived.
    SELECT COUNT(*) INTO v_expected_count FROM tmp_pe_dedupe;

    SELECT COUNT(*) INTO v_archived_count
      FROM paliad.procedural_events pe
     WHERE pe.id IN (SELECT duplicate_id FROM tmp_pe_dedupe)
       AND pe.is_active = false
       AND pe.lifecycle_state = 'archived';

    IF v_archived_count <> v_expected_count THEN
        RAISE EXCEPTION
            '[mig 151] FAILED POST: archived %/% duplicates',
            v_archived_count, v_expected_count;
    END IF;

    -- (c) Acceptance criterion 4: no sequencing_rule still points at
    --     an archived duplicate.
    SELECT COUNT(*) INTO v_orphan_sr
      FROM paliad.sequencing_rules sr
     WHERE sr.procedural_event_id IN (SELECT duplicate_id FROM tmp_pe_dedupe);

    IF v_orphan_sr > 0 THEN
        RAISE EXCEPTION
            '[mig 151] FAILED POST: % sequencing_rules still point at archived PE duplicates',
            v_orphan_sr;
    END IF;

    RAISE NOTICE '[mig 151] OK — archived % duplicates across % name-groups; 0 orphan sequencing_rules',
        v_archived_count,
        (SELECT COUNT(DISTINCT name) FROM tmp_pe_dedupe);
END $$;
