-- t-paliad-205 / Fristen gap-fill — ingest curie's t-paliad-203 deltas
-- as code. Source of truth for the deltas is
-- docs/proposals/fristen-gap-fill-2026-05-18.md § 0.3 (m's decisions
-- captured 2026-05-18, commit 0123d11).
--
-- Mig 093 (commit 40e49e8) retired the Pipeline-A litigation rule
-- corpus and surfaced four open coverage questions for legal review.
-- curie's proposal verified those questions and m signed off on:
--
--   * 4 new rules — preliminary-objection (RoP 19.1) on UPC_INF and
--     UPC_REV, and merits-appeal spawn (RoP 220.1(a)) on the same two
--     proceedings.
--   * 4 polish PATCHes on the German civil-procedure rules — backfill
--     legal_source on de_inf.klage, flip de_inf.erwidg to court-set
--     with a §276 Abs.1 S.2 note, plus a defensive verify on
--     de_inf.berufung.legal_source.
--
-- Final shape per the proposal § 0.3:
--
--   NEW
--     inf.prelim         UPC_INF  parent=inf.soc      1mo  RoP.019.1   flag=with_po       optional
--     rev.prelim         UPC_REV  parent=rev.app      1mo  RoP.019.1   flag=with_po       optional
--     inf.appeal_spawn   UPC_INF  parent=inf.decision 2mo  RoP.220.1.a (no flag, always)  optional  spawn → UPC_APP (id=11)
--     rev.appeal_spawn   UPC_REV  parent=rev.decision 2mo  RoP.220.1.a (no flag, always)  optional  spawn → UPC_APP (id=11)
--
--   PATCH
--     de_inf.klage       legal_source NULL → 'DE.ZPO.253'
--     de_inf.anzeige     no change (already 'DE.ZPO.276.1')
--     de_inf.erwidg      is_court_set false → true; set description with §276 Abs.1 S.2 note
--                        duration_value=6 weeks stays as the default-display value when no
--                        court order is yet attached.
--     de_inf.berufung    legal_source set to 'DE.ZPO.517' if still NULL (defensive verify)
--
-- The merits-appeal spawn rules unconditionally produce the 2-month
-- appeal-window row once inf.decision / rev.decision is anchored
-- (m's F2.3 decision: "appeal is always a possibility"). Visibility
-- filtering for non-appealing projects is a frontend concern, not a
-- rule-level flag (see proposal § 0.3 follow-up note).
--
-- The spawn_proceeding_type_id FK points at UPC_APP (id=11). t-paliad-204
-- may rename the `code` string for that row but the integer id is stable;
-- if id=11 ever moves, this migration's spawn rules still chain to the
-- correct row.
--
-- Idempotency:
--   * Backup snapshot `deadline_rules_pre_095` is CREATE TABLE IF NOT
--     EXISTS, capturing the 4 patched rows at first run.
--   * INSERTs use `WHERE NOT EXISTS` keyed on (proceeding_type_id, code,
--     lifecycle_state='published') — there is no unique index on
--     (proceeding_type_id, code) in paliad.deadline_rules (mig 093 left
--     archived and published rows co-existing with identical codes), so
--     ON CONFLICT is not available; WHERE NOT EXISTS is the equivalent
--     idempotency guard.
--   * UPDATEs are guarded by clauses that only fire when the row still
--     has the old value (legal_source IS NULL, is_court_set = false).
--
-- audit_reason wrapper required by the mig 079 trigger for both UPDATE
-- and INSERT (INSERT defaults to 'create' but we surface the t-paliad-205
-- context anyway so deadline_rule_audit reads cleanly).

SELECT set_config(
    'paliad.audit_reason',
    'mig 095: t-paliad-205 fristen gap-fill — 4 new rules (inf.prelim, rev.prelim, inf.appeal_spawn, rev.appeal_spawn) + 4 patches on de_inf.* rules per docs/proposals/fristen-gap-fill-2026-05-18.md § 0.3',
    true);

-- =============================================================================
-- 1. Backup snapshot of the 4 rows the PATCHes touch. CREATE TABLE IF
--    NOT EXISTS keeps this idempotent across reapplications. Snapshot
--    persists post-patch as the audit anchor; the down migration
--    restores from it.
-- =============================================================================

CREATE TABLE IF NOT EXISTS paliad.deadline_rules_pre_095 AS
SELECT *, now() AS snapshotted_at
  FROM paliad.deadline_rules
 WHERE code IN ('de_inf.klage', 'de_inf.anzeige',
                'de_inf.erwidg', 'de_inf.berufung')
   AND lifecycle_state = 'published'
   AND is_active = true;

COMMENT ON TABLE paliad.deadline_rules_pre_095 IS
    'Snapshot of the 4 de_inf.* deadline_rules rows that mig 095 '
    'PATCHed (t-paliad-205). Source-of-truth for the down migration; '
    'persists post-patch as the permanent audit record. Drop with a '
    'focused follow-up after the gap-fill is verified in prod.';

-- =============================================================================
-- 2. New rules — preliminary objection on UPC_INF and UPC_REV
--    (RoP 19.1, flag-gated `with_po`, 1 month from service of the
--    Statement of Claim / Application for Revocation).
--
--    Anchor: parent_id on the existing root rule (inf.soc / rev.app),
--    matching the chaining pattern used by inf.sod, inf.def_to_ccr,
--    rev.defence, rev.app_to_amend. Idempotent via WHERE NOT EXISTS.
--
--    sequence_order=5 places the PO row before the SoD (sequence_order=10)
--    in the per-proceeding timeline ordering, reflecting the 1-month
--    statutory window beating the 3-month defence in calendar terms.
-- =============================================================================

INSERT INTO paliad.deadline_rules
    (proceeding_type_id, parent_id, code, name, name_en,
     description, primary_party, event_type,
     duration_value, duration_unit, timing,
     rule_code, deadline_notes, deadline_notes_en, sequence_order,
     is_spawn, spawn_proceeding_type_id, spawn_label,
     is_active, legal_source, is_bilateral,
     condition_expr, priority, is_court_set, lifecycle_state)
SELECT
    8,
    (SELECT id FROM paliad.deadline_rules
      WHERE code = 'inf.soc'
        AND proceeding_type_id = 8
        AND lifecycle_state = 'published'
        AND is_active = true),
    'inf.prelim',
    'Vorab-Einrede (R. 19 VerfO)',
    'Preliminary Objection (RoP 19)',
    'Vorab-Einrede des Beklagten gegen Zuständigkeit, Verfahrenssprache (R.14) oder Spruchkörper-Zusammensetzung. Statutarische Frist von 1 Monat ab Zustellung der Klage; der UPC entscheidet typischerweise durch Beschluss vor der Zwischenverhandlung (R.19.7).',
    'defendant',
    'filing',
    1,
    'months',
    'after',
    'RoP.019.1',
    'Innerhalb von 1 Monat ab Zustellung der Klage. Drei mögliche Gründe: (a) Zuständigkeit/Kompetenz, (b) Verfahrenssprache (R.14), (c) Spruchkörper.',
    'Within 1 month of service of the Statement of claim. Three available grounds: (a) jurisdiction/competence, (b) language (R.14), (c) panel composition.',
    5,
    false,
    NULL,
    NULL,
    true,
    'UPC.RoP.19.1',
    false,
    '{"flag":"with_po"}'::jsonb,
    'optional',
    false,
    'published'
WHERE NOT EXISTS (
    SELECT 1 FROM paliad.deadline_rules
     WHERE code = 'inf.prelim'
       AND proceeding_type_id = 8
       AND lifecycle_state = 'published');

INSERT INTO paliad.deadline_rules
    (proceeding_type_id, parent_id, code, name, name_en,
     description, primary_party, event_type,
     duration_value, duration_unit, timing,
     rule_code, deadline_notes, deadline_notes_en, sequence_order,
     is_spawn, spawn_proceeding_type_id, spawn_label,
     is_active, legal_source, is_bilateral,
     condition_expr, priority, is_court_set, lifecycle_state)
SELECT
    9,
    (SELECT id FROM paliad.deadline_rules
      WHERE code = 'rev.app'
        AND proceeding_type_id = 9
        AND lifecycle_state = 'published'
        AND is_active = true),
    'rev.prelim',
    'Vorab-Einrede (R. 19 i.V.m. R. 46 VerfO)',
    'Preliminary Objection (RoP 19 in conjunction with RoP 46)',
    'Vorab-Einrede des Beklagten (Patentinhaber) im Nichtigkeitsverfahren. R.46 erklärt R.19 für Nichtigkeitsverfahren mutatis mutandis anwendbar; statutarische Frist von 1 Monat ab Zustellung der Nichtigkeitsklage.',
    'defendant',
    'filing',
    1,
    'months',
    'after',
    'RoP.019.1',
    'Innerhalb von 1 Monat ab Zustellung der Nichtigkeitsklage. R.46 macht R.19 mutatis mutandis für Nichtigkeitsverfahren anwendbar; in der Praxis vor allem Verfahrenssprache und Spruchkörper-Zusammensetzung als Gründe.',
    'Within 1 month of service of the Application for Revocation. R.46 makes R.19 apply mutatis mutandis to revocation actions; in practice the main grounds are language and panel composition.',
    5,
    false,
    NULL,
    NULL,
    true,
    'UPC.RoP.19.1',
    false,
    '{"flag":"with_po"}'::jsonb,
    'optional',
    false,
    'published'
WHERE NOT EXISTS (
    SELECT 1 FROM paliad.deadline_rules
     WHERE code = 'rev.prelim'
       AND proceeding_type_id = 9
       AND lifecycle_state = 'published');

-- =============================================================================
-- 3. New rules — merits-appeal spawn on UPC_INF and UPC_REV
--    (RoP 220.1(a), 2 months from service of the final decision, always
--    fires once the decision is anchored). spawn_proceeding_type_id=11
--    is UPC_APP; the spawn renders as an entry point into the appeal
--    proceeding which already has app.notice / app.grounds as root
--    rules.
--
--    No condition_expr — m's F2.3 decision: "the appeal deadline should
--    always be triggered by a decision … appeal is always a possibility".
--    Visibility filtering on the frontend is the right place to hide
--    appeals on projects where no appeal is contemplated.
--
--    sequence_order=80 places the spawn row after inf.cost_app (70)
--    and rev.decision's tail in the per-proceeding ordering.
-- =============================================================================

INSERT INTO paliad.deadline_rules
    (proceeding_type_id, parent_id, code, name, name_en,
     description, primary_party, event_type,
     duration_value, duration_unit, timing,
     rule_code, deadline_notes, deadline_notes_en, sequence_order,
     is_spawn, spawn_proceeding_type_id, spawn_label,
     is_active, legal_source, is_bilateral,
     condition_expr, priority, is_court_set, lifecycle_state)
SELECT
    8,
    (SELECT id FROM paliad.deadline_rules
      WHERE code = 'inf.decision'
        AND proceeding_type_id = 8
        AND lifecycle_state = 'published'
        AND is_active = true),
    'inf.appeal_spawn',
    'Berufung gegen Endentscheidung',
    'Appeal against final decision',
    'Berufung gegen die Endentscheidung nach R.118. Statutarische Frist von 2 Monaten ab Zustellung der Entscheidung (R.224.1(a)); die Berufungsbegründung folgt mit 4 Monaten ab Zustellung (R.224.2(a), eigenständige Frist im Berufungsverfahren).',
    'both',
    'filing',
    2,
    'months',
    'after',
    'RoP.220.1.a',
    'Innerhalb von 2 Monaten ab Zustellung der Endentscheidung Berufungsschrift einreichen (R.224.1(a)). Die Berufungsbegründung (R.224.2(a), 4 Monate) läuft als separate Frist im Berufungsverfahren.',
    'Within 2 months of service of the final decision lodge the Statement of appeal (R.224.1(a)). The Statement of grounds (R.224.2(a), 4 months) runs as an independent deadline in the appeal proceeding.',
    80,
    true,
    11,
    'Berufungsverfahren öffnen',
    true,
    'UPC.RoP.220.1',
    false,
    NULL,
    'optional',
    false,
    'published'
WHERE NOT EXISTS (
    SELECT 1 FROM paliad.deadline_rules
     WHERE code = 'inf.appeal_spawn'
       AND proceeding_type_id = 8
       AND lifecycle_state = 'published');

INSERT INTO paliad.deadline_rules
    (proceeding_type_id, parent_id, code, name, name_en,
     description, primary_party, event_type,
     duration_value, duration_unit, timing,
     rule_code, deadline_notes, deadline_notes_en, sequence_order,
     is_spawn, spawn_proceeding_type_id, spawn_label,
     is_active, legal_source, is_bilateral,
     condition_expr, priority, is_court_set, lifecycle_state)
SELECT
    9,
    (SELECT id FROM paliad.deadline_rules
      WHERE code = 'rev.decision'
        AND proceeding_type_id = 9
        AND lifecycle_state = 'published'
        AND is_active = true),
    'rev.appeal_spawn',
    'Berufung gegen Endentscheidung (Nichtigkeit)',
    'Appeal against final decision (revocation)',
    'Berufung gegen die Endentscheidung im Nichtigkeitsverfahren nach R.118. Statutarische Frist von 2 Monaten ab Zustellung der Entscheidung (R.224.1(a)). Bei with_cci-Konstellationen (Verletzungswiderklage) deckt eine R.118-Entscheidung beide Streitgegenstände ab und erzeugt ein gemeinsames Berufungsfenster.',
    'both',
    'filing',
    2,
    'months',
    'after',
    'RoP.220.1.a',
    'Innerhalb von 2 Monaten ab Zustellung der Endentscheidung Berufungsschrift einreichen (R.224.1(a)). Bei Verletzungswiderklage (with_cci) ein gemeinsames Fenster.',
    'Within 2 months of service of the final decision lodge the Statement of appeal (R.224.1(a)). Where a counterclaim for infringement was raised (with_cci) the appeal window covers both parts.',
    80,
    true,
    11,
    'Berufungsverfahren öffnen',
    true,
    'UPC.RoP.220.1',
    false,
    NULL,
    'optional',
    false,
    'published'
WHERE NOT EXISTS (
    SELECT 1 FROM paliad.deadline_rules
     WHERE code = 'rev.appeal_spawn'
       AND proceeding_type_id = 9
       AND lifecycle_state = 'published');

-- =============================================================================
-- 4. PATCHes on existing rows. Each UPDATE is guarded by a WHERE clause
--    that only fires when the row still has the old value — re-running
--    the migration is a no-op once the first run has applied.
-- =============================================================================

-- 4.1 de_inf.klage: legal_source NULL → 'DE.ZPO.253'
UPDATE paliad.deadline_rules
   SET legal_source = 'DE.ZPO.253',
       updated_at   = now()
 WHERE code = 'de_inf.klage'
   AND lifecycle_state = 'published'
   AND is_active = true
   AND legal_source IS NULL;

-- 4.2 de_inf.anzeige: no change — verified DE.ZPO.276.1 already correct
--     (proposal § 4.2; intentional no-op to make the audit log complete).

-- 4.3 de_inf.erwidg: flip is_court_set true; set description with §276
--     Abs.1 S.2 note. Keep duration_value=6, duration_unit='weeks' as
--     the default-display value when no court order is yet attached
--     (per § 0.3 — fristenrechner renders the 6-week heuristic until
--     the user enters the actual court-set date).
UPDATE paliad.deadline_rules
   SET is_court_set = true,
       description  = 'Gericht setzt eine Frist von mindestens zwei Wochen ab Verteidigungsanzeige (§276 Abs. 1 S. 2 ZPO).',
       updated_at   = now()
 WHERE code = 'de_inf.erwidg'
   AND lifecycle_state = 'published'
   AND is_active = true
   AND is_court_set = false;

-- 4.4 de_inf.berufung: defensive verify — set legal_source to
--     'DE.ZPO.517' only if currently NULL. Production value is already
--     'DE.ZPO.517' per the proposal § 4.4 verification, so this is a
--     no-op in prod; preserved here as a belt-and-braces guard against
--     a staging snapshot where the field was never backfilled.
UPDATE paliad.deadline_rules
   SET legal_source = 'DE.ZPO.517',
       updated_at   = now()
 WHERE code = 'de_inf.berufung'
   AND lifecycle_state = 'published'
   AND is_active = true
   AND legal_source IS NULL;

-- =============================================================================
-- 5. Hard assertions. The migration is not safe to leave half-applied —
--    if any of the new rules failed to insert, or the de_inf.erwidg
--    flip didn't land, the fristenrechner corpus is inconsistent.
-- =============================================================================

DO $$
DECLARE
    v_new_rules    integer;
    v_court_set    integer;
    v_appeal_ids   integer;
    v_klage_src    text;
BEGIN
    -- 5.1 All four new rules exist and are active+published
    SELECT count(*) INTO v_new_rules
      FROM paliad.deadline_rules
     WHERE code IN ('inf.prelim', 'rev.prelim',
                    'inf.appeal_spawn', 'rev.appeal_spawn')
       AND is_active = true
       AND lifecycle_state = 'published';
    IF v_new_rules <> 4 THEN
        RAISE EXCEPTION
            'mig 095: expected 4 new active+published rules, got %',
            v_new_rules;
    END IF;

    -- 5.2 de_inf.erwidg is now court-set
    SELECT count(*) INTO v_court_set
      FROM paliad.deadline_rules
     WHERE code = 'de_inf.erwidg'
       AND lifecycle_state = 'published'
       AND is_active = true
       AND is_court_set = true;
    IF v_court_set <> 1 THEN
        RAISE EXCEPTION
            'mig 095: expected de_inf.erwidg to be court-set after patch, got % matching rows',
            v_court_set;
    END IF;

    -- 5.3 Both spawn rules reference an existing proceeding_type id=11
    SELECT count(*) INTO v_appeal_ids
      FROM paliad.deadline_rules dr
      JOIN paliad.proceeding_types pt ON pt.id = dr.spawn_proceeding_type_id
     WHERE dr.code IN ('inf.appeal_spawn', 'rev.appeal_spawn')
       AND dr.lifecycle_state = 'published'
       AND dr.is_active = true
       AND pt.id = 11;
    IF v_appeal_ids <> 2 THEN
        RAISE EXCEPTION
            'mig 095: expected both appeal_spawn rules to chain to proceeding_type id=11, got % matching rows',
            v_appeal_ids;
    END IF;

    -- 5.4 de_inf.klage now has a legal_source (we just set it, or it was
    --     already set — either way it must not be NULL after this mig)
    SELECT legal_source INTO v_klage_src
      FROM paliad.deadline_rules
     WHERE code = 'de_inf.klage'
       AND lifecycle_state = 'published'
       AND is_active = true;
    IF v_klage_src IS NULL THEN
        RAISE EXCEPTION
            'mig 095: de_inf.klage.legal_source is still NULL after patch';
    END IF;
END $$;
