-- 140_drop_deadline_rules — Slice B.4 destructive drop (t-paliad-305 / m/paliad#93)
--
-- HARD STOPS:
--   * Audit-first: snapshot paliad.deadline_rules → paliad.deadline_rules_pre_140
--     in the SAME TRANSACTION as the DROP, per m's snapshot policy
--     (precedent migs 091/093/095/098). The whole .up.sql runs inside a
--     single transaction because the migration runner wraps it; if any
--     statement fails, the snapshot CREATE TABLE rolls back with the
--     destructive DROP.
--   * No data loss: paliad.deadline_rules has been a write-side shadow
--     since B.3 (B.2 dual-write keeps sequencing_rules + procedural_events
--     + legal_sources current). Drift verified clean before this slice
--     (deadline_rules=231, sequencing_rules=231, 0 mismatches across
--     counts/FKs/lifecycle/is_active).
--
-- What this migration does:
--   1. Snapshot deadline_rules → deadline_rules_pre_140 (preserves audit
--      trail of the table's final state for forensic + revert paths).
--   2. Final reconciliation: catch any deadlines whose
--      sequencing_rule_id/procedural_event_id columns drifted from the
--      legacy rule_id (no live drift today — defensive).
--   3. Drop the audit trigger on deadline_rules (it can't fire on a
--      gone table; the trigger function itself stays for the historical
--      paliad.deadline_rule_audit reads).
--   4. Re-point FKs that currently target deadline_rules.id over to
--      sequencing_rules.id. The id values are identical (sequencing_rules
--      inherited deadline_rules.id during mig 136 backfill), so no data
--      migration is needed — just the constraint swap. Affects:
--      - paliad.appointments.deadline_rule_id
--      - paliad.deadline_rule_backfill_orphans.resolved_rule_id
--   5. Drop paliad.deadlines.rule_id column. Per design §5.4 step 16:
--      "DROP COLUMN paliad.deadlines.rule_id (keep rule_code +
--      custom_rule_text as the human-readable denormalized columns —
--      they're the safety net for orphaned deadlines per t-paliad-258)."
--      The new sequencing_rule_id + procedural_event_id columns from
--      mig 136 are the FK back-links from B.4 forward.
--   6. DROP TABLE paliad.deadline_rules.
--   7. INSTEAD OF triggers on paliad.deadline_rules_unified that route
--      INSERTs/UPDATEs to the underlying sr+pe+ls tables. Lets the
--      RuleEditorService keep its existing SQL shape (one INSERT, one
--      UPDATE per write method) with only a table-name swap. The
--      triggers project the legacy column shape back to the three new
--      tables exactly as the dual-write helper did in B.2.
--
-- Down: best-effort restore from the snapshot. The original triggers,
-- indexes, and FKs are NOT recreated — operator must replay historical
-- migrations 078/079/091/095/098/122 to bring the table back to a
-- working shape. The down path is for catastrophic recovery, not casual
-- revert.

-- ---------------------------------------------------------------
-- 1. Snapshot — must precede the destructive ops (same TX).
-- ---------------------------------------------------------------

CREATE TABLE paliad.deadline_rules_pre_140 AS TABLE paliad.deadline_rules;

COMMENT ON TABLE paliad.deadline_rules_pre_140 IS
    'Snapshot of paliad.deadline_rules taken in mig 140 (Slice B.4, '
    't-paliad-305) before the destructive DROP. Mirrors precedent '
    'pre_091/093/095/098. Read-only forensic + revert source.';

-- ---------------------------------------------------------------
-- 2. Final reconciliation — should be a no-op (drift was 0 going
--    into this slice). Belt-and-braces against a write that snuck
--    in between drift-check and this migration.
-- ---------------------------------------------------------------

UPDATE paliad.deadlines d
   SET sequencing_rule_id  = d.rule_id,
       procedural_event_id = sr.procedural_event_id
  FROM paliad.sequencing_rules sr
 WHERE sr.id = d.rule_id
   AND d.rule_id IS NOT NULL
   AND (d.sequencing_rule_id IS DISTINCT FROM d.rule_id
        OR d.procedural_event_id IS DISTINCT FROM sr.procedural_event_id);

-- ---------------------------------------------------------------
-- 3. Drop the deadline_rules audit trigger. The trigger function
--    (paliad.deadline_rule_audit_trigger) stays defined for any
--    historical references; mig 079 created it.
-- ---------------------------------------------------------------

DROP TRIGGER IF EXISTS deadline_rules_audit_aiud ON paliad.deadline_rules;

-- ---------------------------------------------------------------
-- 4. Re-point FKs from deadline_rules → sequencing_rules.
-- ---------------------------------------------------------------

ALTER TABLE paliad.appointments
    DROP CONSTRAINT IF EXISTS appointments_deadline_rule_id_fkey;
ALTER TABLE paliad.appointments
    ADD CONSTRAINT appointments_deadline_rule_id_fkey
        FOREIGN KEY (deadline_rule_id) REFERENCES paliad.sequencing_rules(id);

ALTER TABLE paliad.deadline_rule_backfill_orphans
    DROP CONSTRAINT IF EXISTS deadline_rule_backfill_orphans_resolved_rule_id_fkey;
ALTER TABLE paliad.deadline_rule_backfill_orphans
    ADD CONSTRAINT deadline_rule_backfill_orphans_resolved_rule_id_fkey
        FOREIGN KEY (resolved_rule_id) REFERENCES paliad.sequencing_rules(id);

-- Drop the deadlines→deadline_rules FK before we drop the column.
ALTER TABLE paliad.deadlines
    DROP CONSTRAINT IF EXISTS fristen_rule_id_fkey;

-- ---------------------------------------------------------------
-- 5. Drop paliad.deadlines.rule_id (column + remaining indexes).
-- ---------------------------------------------------------------

ALTER TABLE paliad.deadlines
    DROP COLUMN IF EXISTS rule_id;

-- ---------------------------------------------------------------
-- 6a. Drop the deadline_search materialized view, which has a
--     direct dependency on paliad.deadline_rules (mig 077). We
--     recreate it after the DROP, re-pointed at deadline_rules_unified
--     so reads keep working. All 11 indexes are recreated alongside.
-- ---------------------------------------------------------------

DROP MATERIALIZED VIEW IF EXISTS paliad.deadline_search;

-- ---------------------------------------------------------------
-- 6. DROP TABLE paliad.deadline_rules. Now that:
--    - dependent FKs are re-pointed to sequencing_rules,
--    - the audit trigger is dropped,
--    - deadlines.rule_id is gone,
--    - the deadline_search matview is gone,
--    nothing references the table anymore. The self-FKs
--    (deadline_rules.parent_id, .draft_of) drop with the table.
-- ---------------------------------------------------------------

DROP TABLE paliad.deadline_rules;

-- ---------------------------------------------------------------
-- 7. INSTEAD OF triggers on the view — routes writes to sr+pe+ls.
-- ---------------------------------------------------------------

CREATE OR REPLACE FUNCTION paliad.deadline_rules_unified_insert_trigger()
RETURNS TRIGGER LANGUAGE plpgsql AS $fn$
DECLARE
    v_legal_source_id uuid;
    v_pe_id           uuid;
    v_code            text;
BEGIN
    -- legal_sources upsert (no-op if NEW.legal_source is NULL)
    IF NEW.legal_source IS NOT NULL THEN
        INSERT INTO paliad.legal_sources (citation, jurisdiction)
        VALUES (NEW.legal_source,
                COALESCE(NULLIF(split_part(NEW.legal_source, '.', 1), ''), 'other'))
        ON CONFLICT (citation) DO NOTHING;
        SELECT id INTO v_legal_source_id
          FROM paliad.legal_sources
         WHERE citation = NEW.legal_source;
    END IF;

    -- Mint synthetic code when submission_code is NULL — same recipe
    -- as mig 136 + B.2 dual-write helper. Stays byte-identical.
    v_code := COALESCE(NEW.submission_code,
                       'null.' || substring(replace(NEW.id::text, '-', ''), 1, 8));

    -- procedural_events upsert. ON CONFLICT (code) deliberately leaves
    -- lifecycle_state / published_at / is_active alone — those track
    -- the procedural-event concept's own lifecycle, not the inserting
    -- sequencing-rule's lifecycle (e.g. a CloneAsDraft of a published
    -- rule creates a draft sr that shares the published PE; the PE
    -- should stay 'published'). Identity columns DO update so an
    -- admin editing a draft's name still flips the lawyer-visible
    -- label (1:1 today; revisit when 1:N becomes a real pattern).
    INSERT INTO paliad.procedural_events
        (code, name, name_en, description, event_kind, primary_party_default,
         legal_source_id, concept_id, lifecycle_state, published_at, is_active)
    VALUES
        (v_code, NEW.name, NEW.name_en, NEW.description, NEW.event_type,
         NEW.primary_party, v_legal_source_id, NEW.concept_id,
         COALESCE(NEW.lifecycle_state, 'draft'), NEW.published_at,
         COALESCE(NEW.is_active, true))
    ON CONFLICT (code) DO UPDATE SET
        name                  = EXCLUDED.name,
        name_en               = EXCLUDED.name_en,
        description           = EXCLUDED.description,
        event_kind            = EXCLUDED.event_kind,
        primary_party_default = EXCLUDED.primary_party_default,
        legal_source_id       = EXCLUDED.legal_source_id,
        concept_id            = EXCLUDED.concept_id,
        -- lifecycle_state / published_at / is_active deliberately omitted
        updated_at            = now()
    RETURNING id INTO v_pe_id;

    -- sequencing_rules insert. id is the caller-supplied NEW.id so
    -- existing FK back-links (deadlines.sequencing_rule_id) resolve.
    INSERT INTO paliad.sequencing_rules
        (id, procedural_event_id, proceeding_type_id, parent_id, trigger_event_id,
         duration_value, duration_unit, timing,
         alt_duration_value, alt_duration_unit, alt_rule_code, anchor_alt,
         combine_op, condition_expr, primary_party, sequence_order,
         is_spawn, spawn_label, spawn_proceeding_type_id,
         is_bilateral, is_court_set, priority,
         rule_code, rule_codes, deadline_notes, deadline_notes_en,
         choices_offered, applies_to_target,
         lifecycle_state, draft_of, published_at, is_active,
         created_at, updated_at)
    VALUES
        (NEW.id, v_pe_id, NEW.proceeding_type_id, NEW.parent_id, NEW.trigger_event_id,
         COALESCE(NEW.duration_value, 0), COALESCE(NEW.duration_unit, 'months'),
         COALESCE(NEW.timing, 'after'),
         NEW.alt_duration_value, NEW.alt_duration_unit, NEW.alt_rule_code, NEW.anchor_alt,
         NEW.combine_op, NEW.condition_expr, NEW.primary_party,
         COALESCE(NEW.sequence_order, 0),
         COALESCE(NEW.is_spawn, false), NEW.spawn_label, NEW.spawn_proceeding_type_id,
         COALESCE(NEW.is_bilateral, false), COALESCE(NEW.is_court_set, false),
         COALESCE(NEW.priority, 'mandatory'),
         NEW.rule_code, NEW.rule_codes, NEW.deadline_notes, NEW.deadline_notes_en,
         NEW.choices_offered, NEW.applies_to_target,
         COALESCE(NEW.lifecycle_state, 'draft'), NEW.draft_of,
         NEW.published_at, COALESCE(NEW.is_active, true),
         COALESCE(NEW.created_at, now()), COALESCE(NEW.updated_at, now()));

    RETURN NEW;
END $fn$;

CREATE TRIGGER deadline_rules_unified_insert
    INSTEAD OF INSERT ON paliad.deadline_rules_unified
    FOR EACH ROW EXECUTE FUNCTION paliad.deadline_rules_unified_insert_trigger();


CREATE OR REPLACE FUNCTION paliad.deadline_rules_unified_update_trigger()
RETURNS TRIGGER LANGUAGE plpgsql AS $fn$
DECLARE
    v_legal_source_id uuid;
    v_code            text;
BEGIN
    -- legal_sources upsert (only if NEW.legal_source is non-NULL).
    -- A change FROM non-NULL TO NULL clears legal_source_id on the
    -- procedural_event below — same shape as mig 136 / B.2 behaviour.
    IF NEW.legal_source IS NOT NULL THEN
        INSERT INTO paliad.legal_sources (citation, jurisdiction)
        VALUES (NEW.legal_source,
                COALESCE(NULLIF(split_part(NEW.legal_source, '.', 1), ''), 'other'))
        ON CONFLICT (citation) DO NOTHING;
        SELECT id INTO v_legal_source_id
          FROM paliad.legal_sources
         WHERE citation = NEW.legal_source;
    END IF;

    v_code := COALESCE(NEW.submission_code,
                       'null.' || substring(replace(NEW.id::text, '-', ''), 1, 8));

    -- Update procedural_events keyed by the existing PE link on
    -- sequencing_rules. lifecycle_state / published_at / is_active on
    -- PE are NOT mirrored from the per-sequencing-rule UPDATE — see
    -- the INSERT trigger comment for the rationale (a draft sr that
    -- shares its PE with a published peer must not flip the PE to
    -- draft). Identity columns DO mirror so editing name/code from
    -- the admin UI continues to reach the lawyer-visible label.
    UPDATE paliad.procedural_events
       SET code                  = v_code,
           name                  = NEW.name,
           name_en               = NEW.name_en,
           description           = NEW.description,
           event_kind            = NEW.event_type,
           primary_party_default = NEW.primary_party,
           legal_source_id       = v_legal_source_id,
           concept_id            = NEW.concept_id,
           updated_at            = now()
     WHERE id = (SELECT procedural_event_id
                   FROM paliad.sequencing_rules
                  WHERE id = NEW.id);

    -- Update sequencing_rules (1:1 by id).
    UPDATE paliad.sequencing_rules
       SET proceeding_type_id       = NEW.proceeding_type_id,
           parent_id                = NEW.parent_id,
           trigger_event_id         = NEW.trigger_event_id,
           duration_value           = NEW.duration_value,
           duration_unit            = NEW.duration_unit,
           timing                   = NEW.timing,
           alt_duration_value       = NEW.alt_duration_value,
           alt_duration_unit        = NEW.alt_duration_unit,
           alt_rule_code            = NEW.alt_rule_code,
           anchor_alt               = NEW.anchor_alt,
           combine_op               = NEW.combine_op,
           condition_expr           = NEW.condition_expr,
           primary_party            = NEW.primary_party,
           sequence_order           = NEW.sequence_order,
           is_spawn                 = NEW.is_spawn,
           spawn_label              = NEW.spawn_label,
           spawn_proceeding_type_id = NEW.spawn_proceeding_type_id,
           is_bilateral             = NEW.is_bilateral,
           is_court_set             = NEW.is_court_set,
           priority                 = NEW.priority,
           rule_code                = NEW.rule_code,
           rule_codes               = NEW.rule_codes,
           deadline_notes           = NEW.deadline_notes,
           deadline_notes_en        = NEW.deadline_notes_en,
           choices_offered          = NEW.choices_offered,
           applies_to_target        = NEW.applies_to_target,
           lifecycle_state          = NEW.lifecycle_state,
           draft_of                 = NEW.draft_of,
           published_at             = NEW.published_at,
           is_active                = NEW.is_active,
           updated_at               = now()
     WHERE id = NEW.id;

    RETURN NEW;
END $fn$;

CREATE TRIGGER deadline_rules_unified_update
    INSTEAD OF UPDATE ON paliad.deadline_rules_unified
    FOR EACH ROW EXECUTE FUNCTION paliad.deadline_rules_unified_update_trigger();

-- ---------------------------------------------------------------
-- 8. POST assertions.
-- ---------------------------------------------------------------

DO $$
DECLARE
    v_snapshot_count   int;
    v_sr_count         int;
    v_view_count       int;
    v_dr_table_exists  int;
    v_rule_id_col      int;
BEGIN
    -- B.2 dual-write was implemented only for the active+published lifecycle
    -- (the scope of the read paths and B.4's pre-flip drift check). Archived
    -- + draft rows in deadline_rules were never replicated to sequencing_rules
    -- (they had no production read path). Snapshot includes them all (CREATE
    -- TABLE AS is unfiltered), so we compare on the same filter B.2 actually
    -- maintained. Drafts/archived rows are preserved in paliad.deadline_rules_pre_140
    -- for forensic + future-backfill use.
    SELECT COUNT(*) INTO v_snapshot_count
      FROM paliad.deadline_rules_pre_140
     WHERE is_active = true AND lifecycle_state = 'published';
    SELECT COUNT(*) INTO v_sr_count
      FROM paliad.sequencing_rules
     WHERE is_active = true AND lifecycle_state = 'published';
    SELECT COUNT(*) INTO v_view_count     FROM paliad.deadline_rules_unified;
    IF v_snapshot_count <> v_sr_count THEN
        RAISE EXCEPTION '[mig 140] FAILED POST: snapshot active+published has % rows, sequencing_rules active+published has % rows — dual-write drift',
            v_snapshot_count, v_sr_count;
    END IF;

    SELECT COUNT(*) INTO v_dr_table_exists
      FROM information_schema.tables
     WHERE table_schema = 'paliad' AND table_name = 'deadline_rules';
    IF v_dr_table_exists > 0 THEN
        RAISE EXCEPTION '[mig 140] FAILED POST: paliad.deadline_rules table still exists after DROP';
    END IF;

    SELECT COUNT(*) INTO v_rule_id_col
      FROM information_schema.columns
     WHERE table_schema = 'paliad' AND table_name = 'deadlines' AND column_name = 'rule_id';
    IF v_rule_id_col > 0 THEN
        RAISE EXCEPTION '[mig 140] FAILED POST: paliad.deadlines.rule_id column still exists after DROP';
    END IF;

    RAISE NOTICE '[mig 140] OK — deadline_rules dropped, snapshot=% rows, sequencing_rules=% rows, view (filtered)=% rows, INSTEAD OF triggers active',
        v_snapshot_count, v_sr_count, v_view_count;
END $$;

-- ---------------------------------------------------------------
-- 8. Recreate paliad.deadline_search materialized view against
--    deadline_rules_unified (same column shape — sr.id is the new
--    dr.id, etc.). Definition mirrors mig 077; only the FROM table
--    name changes. All 11 indexes restored.
-- ---------------------------------------------------------------

CREATE MATERIALIZED VIEW paliad.deadline_search AS
 SELECT 'rule'::text AS kind,
    ('r:'::text || (dr.id)::text) AS row_key,
    dc.id AS concept_id,
    dc.slug AS concept_slug,
    dc.name_de AS concept_name_de,
    dc.name_en AS concept_name_en,
    dc.description AS concept_description,
    dc.aliases AS concept_aliases,
    dc.party AS concept_party,
    dc.category AS concept_category,
    dc.sort_order AS concept_sort_order,
    dr.id AS rule_id,
    NULL::bigint AS trigger_event_id,
    pt.code AS proceeding_code,
    pt.name AS proceeding_name_de,
    pt.name_en AS proceeding_name_en,
    pt.jurisdiction,
    pt.display_order AS proceeding_display_order,
    dr.submission_code AS rule_local_code,
    dr.name AS rule_name_de,
    dr.name_en AS rule_name_en,
    dr.legal_source,
    dr.rule_code,
    dr.duration_value,
    dr.duration_unit,
    dr.timing,
    COALESCE(dr.primary_party, dc.party) AS effective_party
   FROM paliad.deadline_rules_unified dr
     JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
     JOIN paliad.deadline_concepts dc ON dc.id = dr.concept_id
  WHERE dr.is_active AND pt.is_active AND pt.category = 'fristenrechner'::text
UNION ALL
 SELECT 'trigger'::text AS kind,
    ('t:'::text || (te.id)::text) AS row_key,
    dc.id AS concept_id,
    dc.slug AS concept_slug,
    dc.name_de AS concept_name_de,
    dc.name_en AS concept_name_en,
    dc.description AS concept_description,
    dc.aliases AS concept_aliases,
    dc.party AS concept_party,
    dc.category AS concept_category,
    dc.sort_order AS concept_sort_order,
    NULL::uuid AS rule_id,
    te.id AS trigger_event_id,
    NULL::text AS proceeding_code,
    NULL::text AS proceeding_name_de,
    NULL::text AS proceeding_name_en,
    'cross-cutting'::text AS jurisdiction,
    9999 AS proceeding_display_order,
    te.code AS rule_local_code,
    te.name_de AS rule_name_de,
    te.name AS rule_name_en,
    dr_trig.legal_source,
    NULL::text AS rule_code,
    NULL::integer AS duration_value,
    NULL::text AS duration_unit,
    NULL::text AS timing,
    dc.party AS effective_party
   FROM paliad.trigger_events te
     JOIN paliad.deadline_concepts dc ON dc.slug = te.concept_id
     LEFT JOIN paliad.deadline_rules_unified dr_trig
       ON dr_trig.trigger_event_id = te.id
      AND dr_trig.proceeding_type_id IS NULL
      AND dr_trig.is_active
      AND dr_trig.lifecycle_state = 'published'::text
  WHERE te.is_active
WITH NO DATA;

CREATE UNIQUE INDEX deadline_search_row_key ON paliad.deadline_search (row_key);
CREATE INDEX deadline_search_concept_id ON paliad.deadline_search (concept_id);
CREATE INDEX deadline_search_proc_code ON paliad.deadline_search (proceeding_code);
CREATE INDEX deadline_search_legal_source ON paliad.deadline_search (legal_source);
CREATE INDEX deadline_search_effective_party ON paliad.deadline_search (effective_party);
CREATE INDEX deadline_search_legal_source_trgm ON paliad.deadline_search USING gin (legal_source gin_trgm_ops);
CREATE INDEX deadline_search_concept_de_trgm ON paliad.deadline_search USING gin (concept_name_de gin_trgm_ops);
CREATE INDEX deadline_search_concept_en_trgm ON paliad.deadline_search USING gin (concept_name_en gin_trgm_ops);
CREATE INDEX deadline_search_rule_de_trgm ON paliad.deadline_search USING gin (rule_name_de gin_trgm_ops);
CREATE INDEX deadline_search_rule_en_trgm ON paliad.deadline_search USING gin (rule_name_en gin_trgm_ops);
CREATE INDEX deadline_search_rule_code_trgm ON paliad.deadline_search USING gin (rule_code gin_trgm_ops);

REFRESH MATERIALIZED VIEW paliad.deadline_search;
