  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,

    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,

    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidates via targeted branches */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx + inserts (with optional chunk limit) */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v18(unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v18(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v18(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v18(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v19(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v19'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weights snapshot */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  /* 1) Normalize inputs */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,

    t.merchant_id::text             AS tx_merchant_raw,

    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,

    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,

    /* Use counterparty/beneficiary only. Exclude payer fields. */
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    /* Tx info blob for fallback name containment */
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,

    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,

    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,

    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* 2) Candidate pairs */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _cand;
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', _count)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  /* 3) Resolve weights */
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags and confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  /* 5) Best per tx and write-out */
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v19(unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v19(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v19(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v19(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v20(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v20'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
BEGIN
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT *
  FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v20(unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v20(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v20(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v20(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function reconciliation_weights_auto_v1(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 90, p_min_rows integer DEFAULT 200) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows       int := 0;
  v_companies  int := 0;
BEGIN
  -- 1) Aggregate recent match stats
  CREATE TEMP TABLE tmp_base AS
  SELECT r.company_id,
         COUNT(*)                                  AS n,
         SUM(s.date_match::int)                    AS hit_date,
         SUM(s.invoice_match::int)                 AS hit_inv,
         SUM(s.order_match::int)                   AS hit_ord,
         SUM(s.merchant_match::int)                AS hit_mer,
         SUM(s.amount_match::int)                  AS hit_amt,
         SUM(s.currency_match::int)                AS hit_cur,
         SUM(s.customer_match::int)                AS hit_cus
  FROM public.reconciliations_match_scores s
  JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
  WHERE (p_company_id IS NULL OR r.company_id = p_company_id)
    AND s.created_at >= now() - make_interval(days => p_days)
  GROUP BY r.company_id
  HAVING COUNT(*) >= p_min_rows;

  -- 2) Smoothed hit rates (Beta(2,3))
  CREATE TEMP TABLE tmp_rates AS
  SELECT company_id, n,
         (hit_date+2.0)/(n+5.0) AS r_date,
         (hit_inv +2.0)/(n+5.0) AS r_inv,
         (hit_ord +2.0)/(n+5.0) AS r_ord,
         (hit_mer +2.0)/(n+5.0) AS r_mer,
         (hit_amt +2.0)/(n+5.0) AS r_amt,
         (hit_cur +2.0)/(n+5.0) AS r_cur,
         (hit_cus +2.0)/(n+5.0) AS r_cus
  FROM tmp_base;

  -- 3) Desired weights per company
  CREATE TEMP TABLE tmp_desired_weights AS
  SELECT company_id, weight_key, weight_value
  FROM (
    SELECT company_id,'date'           ::text AS weight_key, ROUND(GREATEST(0.50, LEAST(1.00, 0.50 + 0.50*r_date))::numeric,2) AS weight_value FROM tmp_rates
    UNION ALL
    SELECT company_id,'invoice_number' ::text, ROUND(GREATEST(1.00, LEAST(1.80, 1.00 + 0.80*r_inv ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'order_number'   ::text, ROUND(GREATEST(0.40, LEAST(1.00, 0.40 + 0.60*r_ord ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'merchant_id'    ::text, ROUND(GREATEST(1.00, LEAST(1.50, 1.00 + 0.50*r_mer ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'amount_total'   ::text, ROUND(GREATEST(1.00, LEAST(1.70, 1.00 + 0.70*r_amt ))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'currency'       ::text, ROUND(GREATEST(0.01, LEAST(0.20, 0.02 + 0.18*r_cur))::numeric,2) FROM tmp_rates
    UNION ALL
    SELECT company_id,'customer_name'  ::text, ROUND(GREATEST(0.50, LEAST(1.00, 0.50 + 0.50*r_cus))::numeric,2) FROM tmp_rates
  ) u;

  -- 4) Upsert into registry (uses your unique constraint on generated column)
  INSERT INTO public.reconciliations_weight_registry
    (company_id, merchant_id, weight_key, weight_value, active, created_at)
  SELECT d.company_id, NULL, d.weight_key, d.weight_value, TRUE, now()
  FROM tmp_desired_weights d
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
    weight_value = EXCLUDED.weight_value,
    active       = TRUE,
    created_at   = now();

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  SELECT COUNT(DISTINCT company_id) INTO v_companies FROM tmp_desired_weights;

  RETURN jsonb_build_object(
    'status','ok',
    'companies_considered', (SELECT COUNT(*) FROM tmp_base),
    'companies_updated',    v_companies,
    'rows_upserted',        COALESCE(v_rows,0),
    'lookback_days',        p_days,
    'min_rows',             p_min_rows,
    'filtered_company',     p_company_id::text
  );
END;
$$;

alter function reconciliation_weights_auto_v1(unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliation_weights_auto_v1(unknown, unknown, unknown) to anon;

grant execute on function reconciliation_weights_auto_v1(unknown, unknown, unknown) to authenticated;

grant execute on function reconciliation_weights_auto_v1(unknown, unknown, unknown) to service_role;

create or replace function reconciliation_weights_auto_v2(p_company_id uuid DEFAULT NULL::uuid, p_days integer DEFAULT 90, p_min_rows integer DEFAULT 200) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_rows      int := 0;
  v_companies int := 0;
BEGIN
  -- 1) Aggregate recent match stats per company
  CREATE TEMP TABLE tmp_base AS
  SELECT r.company_id,
         COUNT(*)                                  AS n,
         SUM(s.date_match::int)                    AS hit_date,
         SUM(s.invoice_match::int)                 AS hit_inv,
         SUM(s.order_match::int)                   AS hit_ord,
         SUM(s.merchant_match::int)                AS hit_mer,
         SUM(s.amount_match::int)                  AS hit_amt,
         SUM(s.currency_match::int)                AS hit_cur,
         SUM(s.customer_match::int)                AS hit_cus
  FROM public.reconciliations_match_scores s
  JOIN public.reconciliations r ON r.reconciliation_id = s.reconciliation_id
  WHERE (p_company_id IS NULL OR r.company_id = p_company_id)
    AND s.created_at >= now() - make_interval(days => p_days)
  GROUP BY r.company_id
  HAVING COUNT(*) >= p_min_rows;

  -- 2) Smoothed rates (Beta(2,3)) to avoid extremes on small samples
  CREATE TEMP TABLE tmp_rates AS
  SELECT company_id, n,
         (hit_date+2.0)/(n+5.0) AS r_date,
         (hit_inv +2.0)/(n+5.0) AS r_inv,
         (hit_ord +2.0)/(n+5.0) AS r_ord,
         (hit_mer +2.0)/(n+5.0) AS r_mer,
         (hit_amt +2.0)/(n+5.0) AS r_amt,
         (hit_cur +2.0)/(n+5.0) AS r_cur,
         (hit_cus +2.0)/(n+5.0) AS r_cus
  FROM tmp_base;

  -- 3) Desired weights with anchored bands (importance-first), capped at 2.00
  -- Bands (min..max):
  --   invoice: 1.6..1.8   order: 1.0..1.5   amount: 1.2..1.7
  --   merchant: 1.0..1.4  date: 0.8..1.2    customer: 0.4..0.8
  --   currency: 0.05..0.20
  CREATE TEMP TABLE tmp_desired_weights AS
  SELECT company_id, weight_key, weight_value
  FROM (
    SELECT company_id,'date'::text            AS weight_key,
           ROUND(LEAST(2.00, GREATEST(0.80, LEAST(1.20, 0.80 + 0.40*r_date)))::numeric, 2) AS weight_value
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'invoice_number'::text,
           ROUND(LEAST(2.00, GREATEST(1.60, LEAST(1.80, 1.60 + 0.20*r_inv)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'order_number'::text,
           ROUND(LEAST(2.00, GREATEST(1.00, LEAST(1.50, 1.00 + 0.50*r_ord)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'merchant_id'::text,
           ROUND(LEAST(2.00, GREATEST(1.00, LEAST(1.40, 1.00 + 0.40*r_mer)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'amount_total'::text,
           ROUND(LEAST(2.00, GREATEST(1.20, LEAST(1.70, 1.20 + 0.50*r_amt)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'currency'::text,
           ROUND(LEAST(2.00, GREATEST(0.05, LEAST(0.20, 0.05 + 0.15*r_cur)))::numeric, 2)
    FROM tmp_rates
    UNION ALL
    SELECT company_id,'customer_name'::text,
           ROUND(LEAST(2.00, GREATEST(0.40, LEAST(0.80, 0.40 + 0.40*r_cus)))::numeric, 2)
    FROM tmp_rates
  ) u;

  -- 4) Upsert (company-level weights; merchant_id NULL)
  INSERT INTO public.reconciliations_weight_registry
    (company_id, merchant_id, weight_key, weight_value, active, created_at)
  SELECT d.company_id, NULL, d.weight_key, d.weight_value, TRUE, now()
  FROM tmp_desired_weights d
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
    weight_value = EXCLUDED.weight_value,
    active       = TRUE,
    created_at   = now();

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  SELECT COUNT(DISTINCT company_id) INTO v_companies FROM tmp_desired_weights;

  RETURN jsonb_build_object(
    'status','ok',
    'companies_considered', (SELECT COUNT(*) FROM tmp_base),
    'companies_updated',    v_companies,
    'rows_upserted',        COALESCE(v_rows,0),
    'lookback_days',        p_days,
    'min_rows',             p_min_rows,
    'filtered_company',     p_company_id::text
  );
END;
$$;

alter function reconciliation_weights_auto_v2(unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliation_weights_auto_v2(unknown, unknown, unknown) to anon;

grant execute on function reconciliation_weights_auto_v2(unknown, unknown, unknown) to authenticated;

grant execute on function reconciliation_weights_auto_v2(unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v21(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v21'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
BEGIN
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  /* representative doc per (company, dedup_serialized) if duplicates disallowed */
  CREATE TEMP TABLE _docs_rep AS
  SELECT DISTINCT ON (company_id_uuid, dedup_serialized)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM _docs
  ORDER BY company_id_uuid, dedup_serialized, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  /* 2) Candidate pairs */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v21(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v21(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v21(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v21(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v22(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v22'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
BEGIN
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  -- representative doc per (company, dedup_key) when duplicates are disallowed
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT
      d.*,
      COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v22(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v22(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v22(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v22(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v23(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v23'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;     -- total ms for this function run (so far) 
  v_ms_each numeric := 0;    -- per-row ms written to reconciliations.processing_time
BEGIN
  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  -- representative doc per (company, dedup_key) when duplicates are disallowed
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT
      d.*,
      COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- INSERT reconciliations, capture inserted IDs, compute ms-per-row, update processing_time
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)   -- or ::bigint if the column is integer
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  -- write match score rows for inserted reconciliations
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v23(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v23(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v23(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v23(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v24(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v24'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;     -- total ms for this function run (so far)
  v_ms_each numeric := 0;    -- per-row ms written to reconciliations.processing_time
  v_run_id uuid;             -- logging run id
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'athena_reconcile_general_v24',
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id        => v_run_id,
    p_action        => 'registry_loaded',
    p_status        => 'ok',
    p_company_id    => p_company_id,
    p_step_ms       => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id      => v_run_id,
    p_action      => 'normalized',
    p_status      => 'ok',
    p_company_id  => p_company_id,
    p_context     => jsonb_build_object(
                      'tx_rows',   (SELECT COUNT(*) FROM _tx),
                      'doc_rows',  (SELECT COUNT(*) FROM _docs),
                      'docs_used', NULL
                    )
  );

  -- representative doc per (company, dedup_key) when duplicates are disallowed
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT
      d.*,
      COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id      => v_run_id,
    p_action      => 'docs_selected',
    p_status      => 'ok',
    p_company_id  => p_company_id,
    p_context     => jsonb_build_object('docs_used', (SELECT COUNT(*) FROM _docs_use),
                                        'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id        => v_run_id,
    p_action        => 'candidates_built',
    p_status        => 'ok',
    p_company_id    => p_company_id,
    p_rows_affected => v_candidates,
    p_context       => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- INSERT reconciliations, capture inserted IDs, compute ms-per-row, update processing_time
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id        => v_run_id,
    p_action        => 'reconciliations_inserted',
    p_status        => 'ok',
    p_company_id    => p_company_id,
    p_rows_affected => v_rows,
    p_context       => jsonb_build_object('per_row_ms', v_ms_each,
                                          'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- write match score rows for inserted reconciliations
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  PERFORM public.reconciliations_log_end(
    p_run_id        => v_run_id,
    p_status        => 'ok',
    p_message       => NULL,
    p_processing_ms => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context       => jsonb_build_object('inserted', v_rows, 'candidates', v_candidates)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id        => v_run_id,
    p_status        => 'error',
    p_message       => SQLERRM,
    p_processing_ms => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context       => jsonb_build_object('inserted_so_far', v_rows, 'candidates', v_candidates)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v24(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v24(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v24(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v24(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function reconciliations_log_start(p_function_version text, p_company_id uuid DEFAULT NULL::uuid, p_context jsonb DEFAULT '{}'::jsonb) returns uuid
    language plpgsql
as
$$
DECLARE v_run_id uuid := gen_random_uuid();
BEGIN
  PERFORM public.reconciliations_log_add(
    p_run_id            => v_run_id,
    p_function_version  => p_function_version,
    p_action            => 'run_start',
    p_status            => 'ok',
    p_target            => 'function',
    p_message           => NULL,
    p_company_id        => p_company_id,
    p_context           => p_context
  );
  RETURN v_run_id;
END;
$$;

alter function reconciliations_log_start(unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliations_log_start(unknown, unknown, unknown) to anon;

grant execute on function reconciliations_log_start(unknown, unknown, unknown) to authenticated;

grant execute on function reconciliations_log_start(unknown, unknown, unknown) to service_role;

create or replace function reconciliations_log_end(p_run_id uuid, p_function_version text, p_status text, p_message text DEFAULT NULL::text, p_processing_ms bigint DEFAULT NULL::bigint, p_context jsonb DEFAULT '{}'::jsonb) returns void
    language sql
as
$$
  SELECT public.reconciliations_log_add(
    p_run_id, p_function_version,
    'run_end', p_status,
    'function', p_message,
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, p_processing_ms, p_context
  );
$$;

alter function reconciliations_log_end(unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliations_log_end(unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliations_log_end(unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliations_log_end(unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v25(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v25'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- write match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v25(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v25(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v25(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v25(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function reconciliations_log_step(p_run_id uuid, p_function_version text, p_action text, p_status text DEFAULT 'ok'::text, p_target text DEFAULT NULL::text, p_message text DEFAULT NULL::text, p_company_id uuid DEFAULT NULL::uuid, p_transaction_id uuid DEFAULT NULL::uuid, p_document_id uuid DEFAULT NULL::uuid, p_reconciliation_id uuid DEFAULT NULL::uuid, p_rows_affected integer DEFAULT NULL::integer, p_confidence numeric DEFAULT NULL::numeric, p_step_ms bigint DEFAULT NULL::bigint, p_processing_ms bigint DEFAULT NULL::bigint, p_context jsonb DEFAULT '{}'::jsonb) returns void
    language sql
as
$$
  SELECT public.reconciliations_log_add(
    p_run_id, p_function_version, p_action, p_status, p_target, p_message,
    p_company_id, p_transaction_id, p_document_id, p_reconciliation_id,
    p_rows_affected, p_confidence, p_step_ms, p_processing_ms, p_context
  );
$$;

alter function reconciliations_log_step(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliations_log_step(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliations_log_step(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliations_log_step(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function reconciliations_staging_update_v2(p_reconciliation_id uuid, p_column text DEFAULT NULL::text, p_column_value boolean DEFAULT NULL::boolean, p_verified boolean DEFAULT NULL::boolean, p_company_id uuid DEFAULT NULL::uuid, p_organization_id uuid DEFAULT NULL::uuid, p_user_id uuid DEFAULT NULL::uuid, p_publish_event boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  -- timing + logging
  v_t0           timestamptz := clock_timestamp();
  v_run_id       uuid;

  v_col              text;
  v_company_id       uuid;
  v_document_id      uuid;
  v_transaction_id   uuid;

  affected_docs      boolean := false;
  affected_tx        boolean := false;

  v_stage_updated     int := 0;
  v_stage_toggle_upd  int := 0;
  v_recon_upd         int := 0;
  v_docs_upd          int := 0;
  v_tx_upd            int := 0;

  j_result jsonb := jsonb_build_object('ok', true);
BEGIN
  /* 0) Start logging */
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'reconciliations_staging_update_v2',
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'reconciliation_id', p_reconciliation_id,
                             'column',            p_column,
                             'column_value',      p_column_value,
                             'verified',          p_verified,
                             'organization_id',   p_organization_id,
                             'user_id',           p_user_id,
                             'publish_event',     p_publish_event
                           )
                         )
  );

  /* 1) Resolve canonical reconciliation row (for links & verified cascade) */
  SELECT r.company_id, r.document_id, r.transaction_id
    INTO v_company_id, v_document_id, v_transaction_id
  FROM public.reconciliations r
  WHERE r.reconciliation_id = p_reconciliation_id;

  IF NOT FOUND THEN
    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'resolve_reconciliation',
      p_status            => 'error',
      p_target            => 'reconciliations_staging',
      p_message           => 'not_found',
      p_company_id        => p_company_id,
      p_transaction_id    => NULL::uuid,
      p_document_id       => NULL::uuid,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => NULL::int,
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => '{}'::jsonb
    );

    PERFORM public.reconciliations_log_end(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_status            => 'error',
      p_message           => 'not_found',
      p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
    );

    RETURN jsonb_build_object('ok', false, 'error', 'not_found');
  END IF;

  /* 2) Optional: toggle one of booked|archived|flagged on staging */
  IF p_column IS NOT NULL THEN
    v_col := CASE lower(p_column)
               WHEN 'booked'   THEN 'booked'
               WHEN 'archived' THEN 'archived'
               WHEN 'flagged'  THEN 'flagged'
               ELSE NULL
             END;

    IF v_col IS NULL THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'toggle_column',
        p_status            => 'error',
        p_target            => 'reconciliations_staging',
        p_message           => 'invalid_column',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => NULL::int,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('provided', p_column)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'invalid_column',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('allowed', jsonb_build_array('booked','archived','flagged'))
      );

      RETURN jsonb_build_object(
        'ok', false,
        'error', 'invalid_column',
        'allowed', jsonb_build_array('booked','archived','flagged')
      );
    END IF;

    IF p_column_value IS NULL THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'toggle_column',
        p_status            => 'error',
        p_target            => 'reconciliations_staging',
        p_message           => 'missing_value',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => NULL::int,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('column', v_col)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'missing_value',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('column', v_col)
      );

      RETURN jsonb_build_object('ok', false, 'error', 'missing_value');
    END IF;

    EXECUTE format(
      'UPDATE public.reconciliations_staging
          SET %I = $1
        WHERE reconciliation_id = $2',
      v_col
    )
    USING p_column_value, p_reconciliation_id;

    GET DIAGNOSTICS v_stage_toggle_upd = ROW_COUNT;

    IF v_stage_toggle_upd = 0 THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'toggle_column',
        p_status            => 'error',
        p_target            => 'reconciliations_staging',
        p_message           => 'staging_row_not_found',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => 0,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('column', v_col, 'value', p_column_value)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'staging_row_not_found',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
      );

      RETURN jsonb_build_object('ok', false, 'error', 'internal', 'message', 'staging_row_not_found');
    END IF;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'toggle_column',
      p_status            => 'ok',
      p_target            => 'reconciliations_staging',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => v_stage_toggle_upd,
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('column', v_col, 'value', p_column_value)
    );

    j_result := j_result
      || jsonb_build_object(
           'reconciliation_id', p_reconciliation_id::text,
           'column', v_col,
           'value', p_column_value
         );
  END IF;

  /* 3) Optional: verified (+ separate logs for flag vs. cascade) */
  IF p_verified IS NOT NULL THEN
    -- Precheck: verifying TRUE but no links
    IF p_verified IS TRUE AND v_document_id IS NULL AND v_transaction_id IS NULL THEN
      PERFORM public.reconciliations_log_step(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_action            => 'verify_precheck',
        p_status            => 'error',
        p_target            => 'reconciliations',
        p_message           => 'no_linked_records',
        p_company_id        => v_company_id,
        p_transaction_id    => v_transaction_id,
        p_document_id       => v_document_id,
        p_reconciliation_id => p_reconciliation_id,
        p_rows_affected     => 0,
        p_confidence        => NULL::numeric,
        p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_processing_ms     => NULL::bigint,
        p_context           => jsonb_build_object('verified', p_verified)
      );

      PERFORM public.reconciliations_log_end(
        p_run_id            => v_run_id,
        p_function_version  => 'reconciliations_staging_update_v2',
        p_status            => 'error',
        p_message           => 'no_linked_records',
        p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
        p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
      );

      RETURN jsonb_build_object('ok', false, 'error', 'no_linked_records');
    END IF;

    -- 3a) Flip verify flags on canonical + staging (VERIFY FLAG STEP)
    UPDATE public.reconciliations r
       SET verified = p_verified
     WHERE r.reconciliation_id = p_reconciliation_id;
    GET DIAGNOSTICS v_recon_upd = ROW_COUNT;

    UPDATE public.reconciliations_staging rs
       SET verified = p_verified
     WHERE rs.reconciliation_id = p_reconciliation_id;
    GET DIAGNOSTICS v_stage_updated = ROW_COUNT;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'verify_flag',
      p_status            => 'ok',
      p_target            => 'reconciliations+staging',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => COALESCE(v_recon_upd,0) + COALESCE(v_stage_updated,0),
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object(
                               'verified', p_verified,
                               'reconciliations_rows', v_recon_upd,
                               'staging_rows', v_stage_updated
                             )
    );

    -- 3b) Cascade to documents (CASCADE DOCS STEP)
    IF v_document_id IS NOT NULL THEN
      UPDATE public.documents_v2 d
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE d.document_id = v_document_id;
      GET DIAGNOSTICS v_docs_upd = ROW_COUNT;
      affected_docs := (v_docs_upd > 0);
    END IF;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'cascade_documents',
      p_status            => CASE WHEN v_document_id IS NULL THEN 'noop' ELSE 'ok' END,
      p_target            => 'documents_v2',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => COALESCE(v_docs_upd,0),
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('verified', p_verified)
    );

    -- 3c) Cascade to transactions (CASCADE TX STEP)
    IF v_transaction_id IS NOT NULL THEN
      UPDATE public.transactions t
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE t.transaction_id = v_transaction_id;
      GET DIAGNOSTICS v_tx_upd = ROW_COUNT;
      affected_tx := (v_tx_upd > 0);
    END IF;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'cascade_transactions',
      p_status            => CASE WHEN v_transaction_id IS NULL THEN 'noop' ELSE 'ok' END,
      p_target            => 'transactions',
      p_message           => NULL::text,
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => COALESCE(v_tx_upd,0),
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('verified', p_verified)
    );

    -- Result payload enrichment
    j_result := j_result
      || jsonb_build_object(
           'verified', p_verified,
           'affected', jsonb_build_object(
             'documents_v2', affected_docs,
             'transactions', affected_tx
           ),
           'rows', jsonb_build_object(
             'reconciliations', v_recon_upd,
             'staging',         v_stage_updated,
             'documents_v2',    v_docs_upd,
             'transactions',    v_tx_upd
           )
         );
  END IF;

  -- If neither branch executed, bad request
  IF p_column IS NULL AND p_verified IS NULL THEN
    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_action            => 'no_op',
      p_status            => 'error',
      p_target            => 'reconciliations_staging',
      p_message           => 'nothing to update',
      p_company_id        => v_company_id,
      p_transaction_id    => v_transaction_id,
      p_document_id       => v_document_id,
      p_reconciliation_id => p_reconciliation_id,
      p_rows_affected     => NULL::int,
      p_confidence        => NULL::numeric,
      p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_processing_ms     => NULL::bigint,
      p_context           => '{}'::jsonb
    );

    PERFORM public.reconciliations_log_end(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_staging_update_v2',
      p_status            => 'error',
      p_message           => 'nothing to update',
      p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
      p_context           => jsonb_build_object('reconciliation_id', p_reconciliation_id)
    );

    RETURN jsonb_build_object('ok', false, 'error', 'no_op', 'message', 'nothing to update');
  END IF;

  -- End logging (success)
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_staging_update_v2',
    p_status            => 'ok',
    p_message           => NULL,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => j_result
  );

  RETURN j_result;

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_staging_update_v2',
    p_status            => 'error',
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'reconciliation_id', p_reconciliation_id,
                             'column', p_column,
                             'verified', p_verified
                           )
  );

  RETURN jsonb_build_object(
    'ok', false,
    'error', 'internal',
    'message', SQLERRM
  );
END;
$$;

alter function reconciliations_staging_update_v2(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliations_staging_update_v2(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliations_staging_update_v2(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliations_staging_update_v2(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function reconciliations_log_add(p_run_id uuid, p_function_version text, p_action text, p_status text, p_target text DEFAULT NULL::text, p_message text DEFAULT NULL::text, p_company_id uuid DEFAULT NULL::uuid, p_transaction_id uuid DEFAULT NULL::uuid, p_document_id uuid DEFAULT NULL::uuid, p_reconciliation_id uuid DEFAULT NULL::uuid, p_rows_affected integer DEFAULT NULL::integer, p_confidence numeric DEFAULT NULL::numeric, p_step_ms bigint DEFAULT NULL::bigint, p_processing_ms bigint DEFAULT NULL::bigint, p_context jsonb DEFAULT '{}'::jsonb) returns void
    language plpgsql
as
$$
BEGIN
  INSERT INTO public.reconciliations_logs (
    reconciliation_log_id, run_id, created_at, function_version,
    action, status, target, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    gen_random_uuid(), p_run_id, now(), p_function_version,
    p_action, p_status, p_target, p_message,
    p_company_id, p_transaction_id, p_document_id, p_reconciliation_id,
    p_rows_affected, p_confidence, p_step_ms, p_processing_ms,
    COALESCE(p_context, '{}'::jsonb)
  );
END;
$$;

alter function reconciliations_log_add(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliations_log_add(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliations_log_add(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliations_log_add(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function reconciliations_weight_registry_upsert_v4(p_weight_key text, p_weight_value numeric, p_company_id uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row       public.reconciliations_weight_registry;
  v_mer       text := NULLIF(btrim(p_merchant_id), '');
  v_exists    boolean;
  v_t0        timestamptz := clock_timestamp();
  v_run_id    uuid;
BEGIN
  -- start logging immediately (even if inputs are invalid, EXCEPTION will close it)
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'reconciliations_weight_registry_upsert_v4',
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'weight_key',    p_weight_key,
                             'weight_value',  p_weight_value,
                             'company_id',    p_company_id,
                             'merchant_id_in',p_merchant_id,
                             'active',        p_active
                           )
                         )
  );

  -- validations (same behavior as before)
  IF p_company_id IS NULL THEN
    RAISE EXCEPTION 'company_id is required' USING ERRCODE='22023';
  END IF;

  IF p_weight_key NOT IN ('date','invoice_number','order_number','merchant_id','amount_total','currency','customer_name') THEN
    RAISE EXCEPTION 'invalid weight_key: %', p_weight_key USING ERRCODE='22023';
  END IF;

  IF p_weight_value IS NULL OR p_weight_value < 0 THEN
    RAISE EXCEPTION 'weight_value must be >= 0. Got %', p_weight_value USING ERRCODE='22003';
  END IF;

  -- log successful validation
  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_action            => 'validated',
    p_status            => 'ok',
    p_target            => 'reconciliations_weight_registry',
    p_message           => NULL,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('merchant_norm', v_mer)
  );

  -- If caller wants to deactivate, only allow if row exists (preserves previous behavior)
  IF p_active IS FALSE THEN
    SELECT TRUE INTO v_exists
    FROM public.reconciliations_weight_registry
    WHERE company_id = p_company_id
      AND ((merchant_id IS NULL AND v_mer IS NULL) OR merchant_id = v_mer)
      AND weight_key = p_weight_key
    LIMIT 1;

    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'reconciliations_weight_registry_upsert_v4',
      p_action            => 'deactivate_check',
      p_status            => CASE WHEN v_exists THEN 'ok' ELSE 'error' END,
      p_target            => 'reconciliations_weight_registry',
      p_message           => CASE WHEN v_exists THEN NULL ELSE 'row_not_found_for_deactivate' END,
      p_company_id        => p_company_id,
      p_context           => jsonb_build_object('merchant_norm', v_mer, 'weight_key', p_weight_key)
    );

    IF NOT v_exists THEN
      RAISE EXCEPTION 'cannot deactivate: row does not exist for (company_id %, merchant_id %, key %)',
        p_company_id, COALESCE(v_mer,'NULL'), p_weight_key USING ERRCODE='02000';
    END IF;
  END IF;

  -- Single UPSERT against unique constraint (same as v3)
  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_mer, p_weight_key, p_weight_value, p_active)
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = EXCLUDED.active
  RETURNING * INTO v_row;

  -- log the upsert
  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_action            => 'upsert',
    p_status            => 'ok',
    p_target            => 'reconciliations_weight_registry',
    p_message           => NULL,
    p_company_id        => p_company_id,
    p_rows_affected     => 1,
    p_step_ms           => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'key',     v_row.weight_key,
                             'value',   v_row.weight_value,
                             'active',  v_row.active,
                             'merchant_id', v_row.merchant_id
                           )
  );

  -- end logging (success)
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_status            => 'ok',
    p_message           => NULL,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'company_id', p_company_id,
                             'merchant_norm', v_mer,
                             'weight_key', p_weight_key
                           )
  );

  RETURN v_row;

EXCEPTION WHEN OTHERS THEN
  -- close the log and rethrow to preserve previous error behavior
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'reconciliations_weight_registry_upsert_v4',
    p_status            => 'error',
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_t0)))::bigint,
    p_context           => jsonb_build_object(
                             'args', jsonb_build_object(
                               'weight_key',    p_weight_key,
                               'weight_value',  p_weight_value,
                               'company_id',    p_company_id,
                               'merchant_id_in',p_merchant_id,
                               'active',        p_active
                             )
                           )
  );
  RAISE;
END;
$$;

alter function reconciliations_weight_registry_upsert_v4(unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliations_weight_registry_upsert_v4(unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliations_weight_registry_upsert_v4(unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliations_weight_registry_upsert_v4(unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function delete_ponto_token(p_user_id integer) returns void
    language plpgsql
as
$$
BEGIN
    DELETE FROM ponto_tokens
    WHERE user_id = p_user_id;
END;
$$;

alter function delete_ponto_token(unknown) owner to postgres;

grant execute on function delete_ponto_token(unknown) to anon;

grant execute on function delete_ponto_token(unknown) to authenticated;

grant execute on function delete_ponto_token(unknown) to service_role;

create or replace function delete_ponto_user(p_user_id text) returns void
    language plpgsql
as
$$ BEGIN -- Delete from tokens
DELETE FROM ponto_tokens
WHERE user_id::TEXT = p_user_id;
-- Delete from user info
DELETE FROM ponto_user_info
WHERE user_id::TEXT = p_user_id;
-- Delete from company mapping
DELETE FROM ponto_user_company_mapping
WHERE user_id::TEXT = p_user_id;
-- Delete from onboarding details
DELETE FROM ponto_onboarding_details
WHERE partner_reference = p_user_id;
-- Delete from account
UPDATE ponto_account
SET revoked = true,
    is_hard_deleted = true,
    deleted_at = EXTRACT(
        EPOCH
        FROM NOW()
    )::BIGINT
WHERE user_id::TEXT = p_user_id;
-- Log success
INSERT INTO public.ponto_logs (
        message,
        status,
        resource,
        user_id,
        action,
        level,
        file
    )
VALUES (
        'Deleted user data from ponto_tokens, ponto_user_info, ponto_user_company_mapping, ponto_onboarding_details, and ponto_account',
        'success',
        'ponto_user',
        p_user_id,
        'delete',
        'info',
        'delete_ponto_user'
    );
EXCEPTION
WHEN OTHERS THEN -- Log failure
INSERT INTO public.ponto_logs (
        message,
        status,
        resource,
        user_id,
        action,
        level,
        file,
        error
    )
VALUES (
        'Failed to delete user data',
        'error',
        'ponto_user',
        p_user_id,
        'delete',
        'error',
        'delete_ponto_user',
        SQLERRM
    );
RAISE;
END;
$$;

alter function delete_ponto_user(unknown) owner to postgres;

grant execute on function delete_ponto_user(unknown) to anon;

grant execute on function delete_ponto_user(unknown) to authenticated;

grant execute on function delete_ponto_user(unknown) to service_role;

create or replace function reconciliations_activity_log_v1(p_run_id uuid, p_company_id uuid, p_transaction_id uuid, p_document_id uuid, p_action text, p_reason text, p_details jsonb DEFAULT '{}'::jsonb) returns uuid
    language plpgsql
as
$$
DECLARE
  v_activity_id uuid := gen_random_uuid();
BEGIN
  INSERT INTO public.reconciliations_activity (
    activity_id, run_id, company_id, transaction_id, document_id, action, reason, details
  )
  VALUES (
    v_activity_id, p_run_id, p_company_id, p_transaction_id, p_document_id, p_action, p_reason, COALESCE(p_details, '{}'::jsonb)
  );
  RETURN v_activity_id;
END;
$$;

alter function reconciliations_activity_log_v1(unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function reconciliations_activity_log_v1(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function reconciliations_activity_log_v1(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function reconciliations_activity_log_v1(unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v26(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v26'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
  rec RECORD;
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- activity: transactions/documents with no candidates
  IF p_log_activity THEN
    FOR rec IN
      SELECT
        tx.company_id_txt::uuid AS company_id,
        tx.transaction_id,
        tx.debited_num, tx.invoice_nr_norm, tx.order_id_norm, tx.tx_iban_ns, tx.tx_bic_ns,
        tx.benef_core_ns, tx.consumer_core_ns, tx.currency_norm_tx, tx.completed_date
      FROM _tx tx
      LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
      WHERE c.transaction_id IS NULL
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id, rec.transaction_id, NULL,
        'considered','no_candidates',
        jsonb_build_object(
          'signals_present', jsonb_build_object(
            'amount',   rec.debited_num IS NOT NULL,
            'invoice',  rec.invoice_nr_norm IS NOT NULL,
            'order',    rec.order_id_norm IS NOT NULL,
            'iban',     rec.tx_iban_ns IS NOT NULL,
            'bic',      rec.tx_bic_ns IS NOT NULL,
            'benef',    rec.benef_core_ns IS NOT NULL,
            'consumer', rec.consumer_core_ns IS NOT NULL,
            'currency', rec.currency_norm_tx IS NOT NULL,
            'date',     rec.completed_date IS NOT NULL
          )
        )
      );
    END LOOP;

    FOR rec IN
      SELECT
        d.company_id_uuid AS company_id,
        d.document_id,
        d.total_num, d.invoice_nr_norm, d.order_id_norm, d.merch_iban_ns, d.merch_bic_ns,
        d.merch_core_ns, d.custinfo_core_ns, d.currency_norm, d.document_unix
      FROM _docs_use d
      LEFT JOIN _cand c ON c.document_id = d.document_id
      WHERE c.document_id IS NULL
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id, NULL, rec.document_id,
        'considered','no_candidates',
        jsonb_build_object(
          'signals_present', jsonb_build_object(
            'amount',   rec.total_num IS NOT NULL,
            'invoice',  rec.invoice_nr_norm IS NOT NULL,
            'order',    rec.order_id_norm IS NOT NULL,
            'iban',     rec.merch_iban_ns IS NOT NULL,
            'bic',      rec.merch_bic_ns IS NOT NULL,
            'merchant', rec.merch_core_ns IS NOT NULL,
            'customer', rec.custinfo_core_ns IS NOT NULL,
            'currency', rec.currency_norm IS NOT NULL,
            'date',     rec.document_unix IS NOT NULL
          )
        )
      );
    END LOOP;
  END IF;

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity: below threshold, lost to winner, doc-level dedup excluded
  IF p_log_activity THEN
    FOR rec IN
      SELECT
        c.company_id_uuid, c.transaction_id, c.document_id,
        c.confidence, c.date_diff_days,
        c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
        c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match
      FROM _conf c
      WHERE c.confidence < p_min_confidence
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','below_min_confidence',
        jsonb_build_object(
          'confidence', rec.confidence,
          'weights', jsonb_build_object(
            'date', rec.date_weight, 'invoice', rec.invoice_weight, 'order', rec.order_weight,
            'merchant', rec.merchant_weight, 'amount', rec.amount_weight,
            'currency', rec.currency_weight, 'customer', rec.customer_weight
          ),
          'matches', jsonb_build_object(
            'date', rec.date_match, 'invoice', rec.invoice_match, 'order', rec.order_match,
            'merchant', rec.merchant_match, 'amount', rec.amount_match,
            'currency', rec.currency_match, 'customer', rec.customer_match
          )
        )
      );
    END LOOP;

    FOR rec IN
      WITH winners AS (
        SELECT company_id_txt, transaction_id, document_id FROM _best
      )
      SELECT
        c.company_id_uuid, c.transaction_id, c.document_id,
        c.confidence, c.date_diff_days
      FROM _conf c
      JOIN winners w
        ON w.company_id_txt = c.company_id_txt
       AND w.transaction_id = c.transaction_id
      WHERE c.confidence >= p_min_confidence
        AND c.document_id <> w.document_id
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','not_top_for_tx',
        jsonb_build_object('confidence', rec.confidence, 'date_diff_days', rec.date_diff_days)
      );
    END LOOP;

    FOR rec IN
      WITH kept AS (SELECT transaction_id, document_id FROM _best_use)
      SELECT
        b.company_id_uuid, b.transaction_id, b.document_id, b.confidence
      FROM _best b
      LEFT JOIN kept k
        ON k.transaction_id = b.transaction_id AND k.document_id = b.document_id
      WHERE k.transaction_id IS NULL
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','doc_dedup_excluded',
        jsonb_build_object('confidence', rec.confidence)
      );
    END LOOP;
  END IF;

  -- precompute already reconciled to log
  CREATE TEMP TABLE _already AS
  SELECT b.company_id_uuid, b.transaction_id, b.document_id
  FROM _best_use b
  JOIN public.reconciliations r
    ON r.company_id = b.company_id_uuid
   AND r.transaction_id = b.transaction_id
   AND r.document_id = b.document_id;

  IF p_log_activity THEN
    FOR rec IN
      SELECT * FROM _already
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id_uuid, rec.transaction_id, rec.document_id,
        'skipped','already_reconciled',
        jsonb_build_object('note','pair exists in public.reconciliations')
      );
    END LOOP;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- write match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    FOR rec IN
      SELECT
        i.company_id, i.transaction_id, i.document_id,
        c.confidence, c.date_diff_days,
        c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match
      FROM _ins i
      JOIN _conf c
        ON c.company_id_uuid = i.company_id
       AND c.transaction_id  = i.transaction_id
       AND c.document_id     = i.document_id
    LOOP
      PERFORM public.reconciliations_activity_log_v1(
        v_run_id, rec.company_id, rec.transaction_id, rec.document_id,
        'matched', NULL,
        jsonb_build_object(
          'confidence', rec.confidence,
          'date_diff_days', rec.date_diff_days,
          'matches', jsonb_build_object(
            'date', rec.date_match, 'invoice', rec.invoice_match, 'order', rec.order_match,
            'merchant', rec.merchant_match, 'amount', rec.amount_match,
            'currency', rec.currency_match, 'customer', rec.customer_match
          )
        )
      );
    END LOOP;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v26(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v26(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v26(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v26(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v27(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v27'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  -- optional: raise statement timeout for this run
  PERFORM set_config('statement_timeout','300000', true); -- 5 min

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,
    NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  -- gather stats for better plans
  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.debited_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based, fast)
  IF p_log_activity THEN
    -- transactions with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, tx.company_id_txt::uuid, tx.transaction_id, NULL,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   tx.debited_num IS NOT NULL,
          'invoice',  tx.invoice_nr_norm IS NOT NULL,
          'order',    tx.order_id_norm IS NOT NULL,
          'iban',     tx.tx_iban_ns IS NOT NULL,
          'bic',      tx.tx_bic_ns IS NOT NULL,
          'benef',    tx.benef_core_ns IS NOT NULL,
          'consumer', tx.consumer_core_ns IS NOT NULL,
          'currency', tx.currency_norm_tx IS NOT NULL,
          'date',     tx.completed_date IS NOT NULL
        )
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    -- documents with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, d.company_id_uuid, NULL, d.document_id,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   d.total_num IS NOT NULL,
          'invoice',  d.invoice_nr_norm IS NOT NULL,
          'order',    d.order_id_norm IS NOT NULL,
          'iban',     d.merch_iban_ns IS NOT NULL,
          'bic',      d.merch_bic_ns IS NOT NULL,
          'merchant', d.merch_core_ns IS NOT NULL,
          'customer', d.custinfo_core_ns IS NOT NULL,
          'currency', d.currency_norm IS NOT NULL,
          'date',     d.document_unix IS NOT NULL
        )
      )
    FROM _docs_use d
    LEFT JOIN _cand c ON c.document_id = d.document_id
    WHERE c.document_id IS NULL;

    -- below min confidence
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'confidence', c.confidence,
        'weights', jsonb_build_object(
          'date', c.date_weight, 'invoice', c.invoice_weight, 'order', c.order_weight,
          'merchant', c.merchant_weight, 'amount', c.amount_weight,
          'currency', c.currency_weight, 'customer', c.customer_weight
        ),
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _conf c
    WHERE c.confidence < p_min_confidence;

    -- not top for transaction
    WITH winners AS (
      SELECT company_id_txt, transaction_id, document_id FROM _best
    )
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object('confidence', c.confidence, 'date_diff_days', c.date_diff_days)
    FROM _conf c
    JOIN winners w
      ON w.company_id_txt = c.company_id_txt
     AND w.transaction_id = c.transaction_id
    WHERE c.confidence >= p_min_confidence
      AND c.document_id <> w.document_id;

    -- dropped by doc-level dedup
    WITH kept AS (SELECT transaction_id, document_id FROM _best_use)
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, b.company_id_uuid, b.transaction_id, b.document_id,
      'skipped','doc_dedup_excluded',
      jsonb_build_object('confidence', b.confidence)
    FROM _best b
    LEFT JOIN kept k
      ON k.transaction_id = b.transaction_id AND k.document_id = b.document_id
    WHERE k.transaction_id IS NULL;

    -- already reconciled
    CREATE TEMP TABLE _already AS
    SELECT b.company_id_uuid, b.transaction_id, b.document_id
    FROM _best_use b
    JOIN public.reconciliations r
      ON r.company_id = b.company_id_uuid
     AND r.transaction_id = b.transaction_id
     AND r.document_id = b.document_id;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, a.company_id_uuid, a.transaction_id, a.document_id,
      'skipped','already_reconciled',
      jsonb_build_object('note','pair exists in public.reconciliations')
    FROM _already a;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, i.company_id, i.transaction_id, i.document_id,
      'matched', NULL,
      jsonb_build_object(
        'confidence', c.confidence,
        'date_diff_days', c.date_diff_days,
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _ins i
    JOIN _conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v27(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v27(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v27(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v27(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v28(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v28'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false, p_amount_tol_pct numeric DEFAULT 0.005, p_amount_tol_floor numeric DEFAULT 0.01, p_tol_requires_strong boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  -- optional: raise statement timeout for this run
  PERFORM set_config('statement_timeout','300000', true); -- 5 min

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity,
                             'amount_tol_pct',   p_amount_tol_pct,
                             'amount_tol_floor', p_amount_tol_floor,
                             'tol_requires_strong', p_tol_requires_strong
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,

    NULLIF(regexp_replace(t.debited::text,  '[^\d\.-]', '', 'g'), '')::numeric  AS debited_num,
    NULLIF(regexp_replace(t.credited::text, '[^\d\.-]', '', 'g'), '')::numeric  AS credited_num,
    COALESCE(
      NULLIF(regexp_replace(t.debited::text,  '[^\d\.-]', '', 'g'), '')::numeric,
      NULLIF(regexp_replace(t.credited::text, '[^\d\.-]', '', 'g'), '')::numeric,
      NULLIF(regexp_replace(t.money_out::text,'[^\d\.-]', '', 'g'), '')::numeric,
      NULLIF(regexp_replace(t.money_in::text, '[^\d\.-]', '', 'g'), '')::numeric
    ) AS amount_num,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (amount_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents (+ dedup_serialized)
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs

  -- exact amount
  CREATE TEMP TABLE _cand_amount_exact AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND d.total_num = tx.amount_num;

  -- tolerant amount (percent), requires currency match and a strong cue unless disabled
  CREATE TEMP TABLE _cand_amount_tol AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND tx.currency_norm_tx = d.currency_norm
   AND abs(d.total_num - tx.amount_num) <= GREATEST(p_amount_tol_floor, d.total_num * p_amount_tol_pct)
   AND (
        NOT p_tol_requires_strong
        OR (
             (tx.invoice_nr_norm IS NOT NULL AND d.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = d.invoice_nr_norm)
          OR (tx.order_id_norm   IS NOT NULL AND d.order_id_norm   IS NOT NULL AND tx.order_id_norm   = d.order_id_norm)
          OR (tx.tx_iban_ns IS NOT NULL AND d.merch_iban_ns IS NOT NULL AND tx.tx_iban_ns = d.merch_iban_ns)
          OR (tx.tx_bic_ns  IS NOT NULL AND d.merch_bic_ns  IS NOT NULL AND tx.tx_bic_ns  = d.merch_bic_ns)
          OR (
               tx.benef_core_ns IS NOT NULL AND d.merch_core_ns IS NOT NULL
           AND length(tx.benef_core_ns) >= 3 AND length(d.merch_core_ns) >= 3
           AND (tx.benef_core_ns = d.merch_core_ns
                OR position(d.merch_core_ns in tx.benef_core_ns) > 0
                OR position(tx.benef_core_ns in d.merch_core_ns) > 0)
          )
          OR (
               tx.consumer_core_ns IS NOT NULL AND (
                 (d.custinfo_core_ns IS NOT NULL AND (
                     tx.consumer_core_ns = d.custinfo_core_ns
                     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
                     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
                 OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
               )
          )
        )
       );

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount_exact
    UNION ALL SELECT * FROM _cand_amount_tol
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    abs(c.total_num - c.amount_num) AS amount_delta,
    CASE WHEN c.total_num IS NULL OR c.amount_num IS NULL THEN NULL
         ELSE abs(c.total_num - c.amount_num) / NULLIF(c.total_num,0)
    END AS amount_delta_pct,
    (c.amount_num IS NOT NULL AND c.total_num IS NOT NULL
       AND abs(c.total_num - c.amount_num) <= GREATEST(p_amount_tol_floor, c.total_num * p_amount_tol_pct)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,
    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match
          THEN rw.amount_weight *
               GREATEST(0, 1 - (
                 s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
               ))
          ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,
    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match
            THEN rw.amount_weight *
                 GREATEST(0, 1 - (
                   s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                 ))
            ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,
    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match
                    THEN rw.amount_weight *
                         GREATEST(0, 1 - (
                           s.amount_delta / GREATEST(p_amount_tol_floor, s.total_num * p_amount_tol_pct)
                         ))
                    ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,
    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days,
    s.amount_delta
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

  CREATE INDEX ON _conf (company_id_txt, transaction_id, confidence DESC, numerator DESC);

  -- best per transaction (+ optional doc-level dedup)
  CREATE TEMP TABLE _best AS
  WITH best_per_tx AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
      c.company_id_txt, c.company_id_uuid, c.transaction_id, c.document_id,
      c.confidence, c.numerator, c.total_weight, c.currency_weight, c.date_diff_days,
      c.fiscal_year, c.quarter, c.month_text, c.amount_delta
    FROM _conf c
    WHERE c.confidence >= p_min_confidence
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             amount_delta ASC,
             date_diff_days ASC,
             document_id
  ),
  ranked AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        ORDER BY
          b.confidence DESC,
          (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
          b.amount_delta ASC,
          b.date_diff_days ASC,
          b.document_id
      ) AS rn
    FROM best_per_tx b
  )
  SELECT * FROM ranked
  WHERE p_chunk_limit IS NULL OR rn <= p_chunk_limit;

  CREATE TEMP TABLE _best_use (LIKE _best INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _best_use SELECT * FROM _best;
  ELSE
    WITH pick_one AS (
      SELECT b.*,
             ROW_NUMBER() OVER (
               PARTITION BY b.company_id_uuid, b.document_id
               ORDER BY b.confidence DESC,
                        (CASE WHEN b.numerator >= (b.total_weight - b.currency_weight) THEN 1 ELSE 0 END) DESC,
                        b.amount_delta ASC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use
    SELECT company_id_txt, company_id_uuid, transaction_id, document_id,
           confidence, numerator, total_weight, currency_weight, date_diff_days,
           fiscal_year, quarter, month_text, rn, amount_delta
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based)
  IF p_log_activity THEN
    -- transactions with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, tx.company_id_txt::uuid, tx.transaction_id, NULL,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   tx.amount_num IS NOT NULL,
          'invoice',  tx.invoice_nr_norm IS NOT NULL,
          'order',    tx.order_id_norm IS NOT NULL,
          'iban',     tx.tx_iban_ns IS NOT NULL,
          'bic',      tx.tx_bic_ns IS NOT NULL,
          'benef',    tx.benef_core_ns IS NOT NULL,
          'consumer', tx.consumer_core_ns IS NOT NULL,
          'currency', tx.currency_norm_tx IS NOT NULL,
          'date',     tx.completed_date IS NOT NULL
        )
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    -- documents with no candidates
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, d.company_id_uuid, NULL, d.document_id,
      'considered','no_candidates',
      jsonb_build_object(
        'signals_present', jsonb_build_object(
          'amount',   d.total_num IS NOT NULL,
          'invoice',  d.invoice_nr_norm IS NOT NULL,
          'order',    d.order_id_norm IS NOT NULL,
          'iban',     d.merch_iban_ns IS NOT NULL,
          'bic',      d.merch_bic_ns IS NOT NULL,
          'merchant', d.merch_core_ns IS NOT NULL,
          'customer', d.custinfo_core_ns IS NOT NULL,
          'currency', d.currency_norm IS NOT NULL,
          'date',     d.document_unix IS NOT NULL
        )
      )
    FROM _docs_use d
    LEFT JOIN _cand c ON c.document_id = d.document_id
    WHERE c.document_id IS NULL;

    -- below min confidence
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','below_min_confidence',
      jsonb_build_object(
        'confidence', c.confidence,
        'amount_delta', c.amount_delta,
        'weights', jsonb_build_object(
          'date', c.date_weight, 'invoice', c.invoice_weight, 'order', c.order_weight,
          'merchant', c.merchant_weight, 'amount', c.amount_weight,
          'currency', c.currency_weight, 'customer', c.customer_weight
        ),
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _conf c
    WHERE c.confidence < p_min_confidence;

    -- not top for transaction
    WITH winners AS (
      SELECT company_id_txt, transaction_id, document_id FROM _best
    )
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, c.company_id_uuid, c.transaction_id, c.document_id,
      'skipped','not_top_for_tx',
      jsonb_build_object('confidence', c.confidence, 'date_diff_days', c.date_diff_days, 'amount_delta', c.amount_delta)
    FROM _conf c
    JOIN winners w
      ON w.company_id_txt = c.company_id_txt
     AND w.transaction_id = c.transaction_id
    WHERE c.confidence >= p_min_confidence
      AND c.document_id <> w.document_id;

    -- dropped by doc-level dedup
    WITH kept AS (SELECT transaction_id, document_id FROM _best_use)
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, b.company_id_uuid, b.transaction_id, b.document_id,
      'skipped','doc_dedup_excluded',
      jsonb_build_object('confidence', b.confidence, 'amount_delta', b.amount_delta)
    FROM _best b
    LEFT JOIN kept k
      ON k.transaction_id = b.transaction_id AND k.document_id = b.document_id
    WHERE k.transaction_id IS NULL;

    -- already reconciled
    CREATE TEMP TABLE _already AS
    SELECT b.company_id_uuid, b.transaction_id, b.document_id
    FROM _best_use b
    JOIN public.reconciliations r
      ON r.company_id = b.company_id_uuid
     AND r.transaction_id = b.transaction_id
     AND r.document_id = b.document_id;

    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, a.company_id_uuid, a.transaction_id, a.document_id,
      'skipped','already_reconciled',
      jsonb_build_object('note','pair exists in public.reconciliations')
    FROM _already a;
  END IF;

  -- INSERT reconciliations
  CREATE TEMP TABLE _ins AS
  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, p_weightset_id,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best_use b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  SELECT * FROM ins;

  SELECT COUNT(*) INTO v_rows FROM _ins;

  v_run_ms  := ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint;
  v_ms_each := ROUND((v_run_ms::numeric) / GREATEST(v_rows, 1), 0);

  UPDATE public.reconciliations r
  SET processing_time = v_ms_each::numeric(20,0)
  FROM _ins i
  WHERE r.reconciliation_id = i.reconciliation_id;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'reconciliations_inserted'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_rows::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('per_row_ms', v_ms_each, 'allow_doc_dups', p_allow_doc_duplicates)
  );

  -- match scores
  INSERT INTO public.reconciliations_match_scores (
    reconciliations_match_score_id,
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match, customer_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight, customer_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score, customer_score
  )
  SELECT
    gen_random_uuid(),
    i.reconciliation_id,
    p_weightset_id,
    c.confidence,
    c.numerator,
    c.total_weight,
    c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match, c.customer_match,
    c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight, c.customer_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_score, c.customer_score
  FROM _ins i
  JOIN _conf c
    ON c.company_id_uuid = i.company_id
   AND c.transaction_id  = i.transaction_id
   AND c.document_id     = i.document_id;

  -- activity: final matches
  IF p_log_activity THEN
    INSERT INTO public.reconciliations_activity
      (activity_id, run_id, company_id, transaction_id, document_id, action, reason, details)
    SELECT
      gen_random_uuid(), v_run_id, i.company_id, i.transaction_id, i.document_id,
      'matched', NULL,
      jsonb_build_object(
        'confidence', c.confidence,
        'date_diff_days', c.date_diff_days,
        'amount_delta', c.amount_delta,
        'matches', jsonb_build_object(
          'date', c.date_match, 'invoice', c.invoice_match, 'order', c.order_match,
          'merchant', c.merchant_match, 'amount', c.amount_match,
          'currency', c.currency_match, 'customer', c.customer_match
        )
      )
    FROM _ins i
    JOIN _conf c
      ON c.company_id_uuid = i.company_id
     AND c.transaction_id  = i.transaction_id
     AND c.document_id     = i.document_id;
  END IF;

  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'ok'::text,
    p_message           => NULL::text,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'status', v_status,
    'message', v_msg,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings,
    'candidates', v_candidates,
    'reconciliations_processed', v_rows,
    'chunk', jsonb_build_object(
      'limit', p_chunk_limit,
      'applied', (p_chunk_limit IS NOT NULL),
      'selected', (SELECT COUNT(*) FROM _best_use)
    )
  );

EXCEPTION WHEN OTHERS THEN
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_status            => 'error'::text,
    p_message           => SQLERRM,
    p_processing_ms     => ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    p_context           => jsonb_build_object('inserted_so_far', v_rows::int, 'candidates', v_candidates::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_processed', 0,
    'status', 'error',
    'message', SQLERRM,
    'processing_time_ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint
  );
END;
$$;

alter function athena_reconcile_general_v28(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

grant execute on function athena_reconcile_general_v28(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to anon;

grant execute on function athena_reconcile_general_v28(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to authenticated;

grant execute on function athena_reconcile_general_v28(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) to service_role;

create or replace function athena_reconcile_general_v29(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v29'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer, p_allow_doc_duplicates boolean DEFAULT false, p_log_activity boolean DEFAULT false, p_amount_tol_pct numeric DEFAULT 0.005, p_amount_tol_floor numeric DEFAULT 0.01, p_tol_requires_strong boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  v_candidates bigint := 0;
  v_run_ms  bigint := 0;
  v_ms_each numeric := 0;
  v_run_id uuid;
BEGIN
  PERFORM set_config('statement_timeout','300000', true); -- 5 min

  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => p_athena_view,
    p_company_id       => p_company_id,
    p_context          => jsonb_build_object(
                           'args', jsonb_build_object(
                             'date_window_days', p_date_window_days,
                             'min_confidence',   p_min_confidence,
                             'chunk_limit',      p_chunk_limit,
                             'allow_doc_dups',   p_allow_doc_duplicates,
                             'log_activity',     p_log_activity,
                             'amount_tol_pct',   p_amount_tol_pct,
                             'amount_tol_floor', p_amount_tol_floor,
                             'tol_requires_strong', p_tol_requires_strong
                           )
                         )
  );

  -- weights registry snapshots
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

  CREATE TEMP TABLE _reg_company AS
  SELECT
    company_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS comp_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS comp_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS comp_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS comp_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS comp_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS comp_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS comp_customer
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
  GROUP BY company_id;

  CREATE TEMP TABLE _reg_merchant AS
  SELECT
    company_id, merchant_id,
    MAX(CASE WHEN weight_key='date'           THEN weight_value END) AS mer_date,
    MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END) AS mer_invoice,
    MAX(CASE WHEN weight_key='order_number'   THEN weight_value END) AS mer_order,
    MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END) AS mer_merchant,
    MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END) AS mer_amount,
    MAX(CASE WHEN weight_key='currency'       THEN weight_value END) AS mer_currency,
    MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END) AS mer_customer
  FROM public.reconciliations_weight_registry
  WHERE active
  GROUP BY company_id, merchant_id;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'registry_loaded'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'company_rows',  (SELECT COUNT(*)::int FROM _reg_company),
                            'merchant_rows', (SELECT COUNT(*)::int FROM _reg_merchant)
                          )
  );

  -- normalize transactions with direction
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    t.completed_date::bigint        AS completed_date,
    UPPER(NULLIF(t.currency, ''))   AS currency_norm_tx,

    NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) AS credited_num,
    NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) AS money_in_num,
    NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0) AS debited_num,
    NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0) AS money_out_num,

    COALESCE(
      NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0),
      NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0)
    ) AS amount_num,

    (NULLIF(NULLIF(regexp_replace(t.credited::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL
     OR NULLIF(NULLIF(regexp_replace(t.money_in::text,  '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL)  AS tx_is_in,
    (NULLIF(NULLIF(regexp_replace(t.debited::text,   '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL
     OR NULLIF(NULLIF(regexp_replace(t.money_out::text, '[^\d\.-]', '', 'g'), '')::numeric, 0) IS NOT NULL) AS tx_is_out,

    public.norm_code(t.invoice_nr)  AS invoice_nr_norm,
    public.norm_code(t.order_id)    AS order_id_norm,
    t.merchant_id::text             AS tx_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ), '') AS benef_core_ns,
    NULLIF(public.norm_text_ns(t.name_consumer), '') AS consumer_core_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty,  t.bic_beneficiary,  NULLIF(t.bic,''))),  '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,
    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR t.company_id::uuid = p_company_id);

  CREATE INDEX ON _tx (company_id_txt);
  CREATE INDEX ON _tx (completed_date);
  CREATE INDEX ON _tx (invoice_nr_norm);
  CREATE INDEX ON _tx (order_id_norm);
  CREATE INDEX ON _tx (currency_norm_tx);
  CREATE INDEX ON _tx (benef_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (amount_num);
  CREATE INDEX ON _tx (tx_iban_ns);
  CREATE INDEX ON _tx (tx_bic_ns);
  CREATE INDEX ON _tx (info_tx_ns);

  -- normalize documents
  CREATE TEMP TABLE _docs AS
  SELECT
    d.document_id::uuid                        AS document_id,
    d.company_id::uuid                         AS company_id_uuid,
    public.parse_doc_date(d.document_date, CASE WHEN d.document_date_unix IS NULL THEN NULL ELSE d.document_date_unix::bigint END) AS document_unix,
    UPPER(NULLIF(d.currency, ''))              AS currency_norm,
    ROUND(d.total::numeric, 2)                 AS total_num,
    public.norm_code(d.invoice_number)         AS invoice_nr_norm,
    public.norm_code(d.order_id)               AS order_id_norm,
    d.merchant_name::text                      AS doc_merchant_raw,
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,
    NULLIF(public.norm_iban(d.merchant_info::jsonb ->> 'iban'), '') AS merch_iban_ns,
    NULLIF(regexp_replace(UPPER(d.merchant_info::jsonb ->> 'bic'), '[^A-Z0-9]', '', 'g'), '') AS merch_bic_ns,
    NULLIF(public.norm_text_ns(
      concat_ws(' ',
        (d.customer_info::jsonb ->> 'name'),
        (d.customer_info::jsonb ->> 'contact_person'),
        (d.customer_info::jsonb ->> 'company_name'),
        (d.customer_info::jsonb ->> 'email')
      )
    ), '') AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns,
    d.dedup_serialized::text                   AS dedup_serialized
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (merch_iban_ns);
  CREATE INDEX ON _docs (merch_bic_ns);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);
  CREATE INDEX ON _docs (info_ns);
  CREATE INDEX ON _docs (dedup_serialized);

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'normalized'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => NULL::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint),
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object(
                            'tx_rows',  (SELECT COUNT(*)::int FROM _tx),
                            'doc_rows', (SELECT COUNT(*)::int FROM _docs)
                          )
  );

  -- representative doc per (company, dedup_key)
  CREATE TEMP TABLE _docs_rep AS
  WITH keyed AS (
    SELECT d.*, COALESCE(NULLIF(d.dedup_serialized, ''), d.document_id::text) AS dedup_key
    FROM _docs d
  )
  SELECT DISTINCT ON (company_id_uuid, dedup_key)
         document_id, company_id_uuid, document_unix, currency_norm, total_num,
         invoice_nr_norm, order_id_norm, doc_merchant_raw, merch_core_ns,
         merch_iban_ns, merch_bic_ns, custinfo_core_ns, info_ns, dedup_serialized
  FROM keyed
  ORDER BY company_id_uuid, dedup_key, document_id;

  CREATE TEMP TABLE _docs_use (LIKE _docs INCLUDING ALL);
  IF p_allow_doc_duplicates THEN
    INSERT INTO _docs_use SELECT * FROM _docs;
  ELSE
    INSERT INTO _docs_use SELECT * FROM _docs_rep;
  END IF;

  CREATE INDEX ON _docs_use (company_id_uuid);
  CREATE INDEX ON _docs_use (document_unix);
  CREATE INDEX ON _docs_use (invoice_nr_norm);
  CREATE INDEX ON _docs_use (order_id_norm);
  CREATE INDEX ON _docs_use (currency_norm);
  CREATE INDEX ON _docs_use (merch_core_ns);
  CREATE INDEX ON _docs_use (merch_iban_ns);
  CREATE INDEX ON _docs_use (merch_bic_ns);
  CREATE INDEX ON _docs_use (custinfo_core_ns);
  CREATE INDEX ON _docs_use (total_num);
  CREATE INDEX ON _docs_use (info_ns);
  CREATE INDEX ON _docs_use (dedup_serialized);

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'docs_selected'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => (SELECT COUNT(*)::int FROM _docs_use),
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('allow_doc_dups', p_allow_doc_duplicates)
  );

  -- candidate pairs

  -- exact amount
  CREATE TEMP TABLE _cand_amount_exact AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND d.total_num = tx.amount_num;

  -- tolerant amount (percent), currency match + strong cue (direction-aware)
  CREATE TEMP TABLE _cand_amount_tol AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.amount_num IS NOT NULL
   AND tx.currency_norm_tx = d.currency_norm
   AND abs(d.total_num - tx.amount_num) <= GREATEST(p_amount_tol_floor, d.total_num * p_amount_tol_pct)
   AND (
        NOT p_tol_requires_strong
        OR (
             (tx.invoice_nr_norm IS NOT NULL AND d.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = d.invoice_nr_norm)
          OR (tx.order_id_norm   IS NOT NULL AND d.order_id_norm   IS NOT NULL AND tx.order_id_norm   = d.order_id_norm)
          OR (tx.tx_iban_ns IS NOT NULL AND d.merch_iban_ns IS NOT NULL AND tx.tx_iban_ns = d.merch_iban_ns)
          OR (tx.tx_bic_ns  IS NOT NULL AND d.merch_bic_ns  IS NOT NULL AND tx.tx_bic_ns  = d.merch_bic_ns)
          OR (tx.tx_is_out AND tx.benef_core_ns IS NOT NULL AND d.merch_core_ns IS NOT NULL AND
              (tx.benef_core_ns = d.merch_core_ns
               OR position(d.merch_core_ns in tx.benef_core_ns) > 0
               OR position(tx.benef_core_ns in d.merch_core_ns) > 0))
          OR (tx.tx_is_in AND tx.consumer_core_ns IS NOT NULL AND
              ( (d.custinfo_core_ns IS NOT NULL AND
                  (tx.consumer_core_ns = d.custinfo_core_ns
                   OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
                   OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
                OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)))
        )
       );

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND (
        (d.custinfo_core_ns IS NOT NULL AND (
           tx.consumer_core_ns = d.custinfo_core_ns
           OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
           OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0))
        OR (d.info_ns IS NOT NULL AND position(tx.consumer_core_ns in d.info_ns) > 0)
   );

  CREATE TEMP TABLE _cand_iban AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND d.merch_iban_ns IS NOT NULL
     AND tx.tx_iban_ns = d.merch_iban_ns;

  CREATE TEMP TABLE _cand_bic AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND d.merch_bic_ns IS NOT NULL
     AND tx.tx_bic_ns = d.merch_bic_ns;

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.*, d.document_id, d.company_id_uuid, d.document_unix, d.currency_norm,
    d.total_num, d.invoice_nr_norm AS doc_invoice, d.order_id_norm AS doc_order,
    d.doc_merchant_raw, d.merch_core_ns, d.merch_iban_ns, d.merch_bic_ns, d.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs_use d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  CREATE TEMP TABLE _cand AS
  SELECT DISTINCT ON (transaction_id, document_id) *
  FROM (
    SELECT * FROM _cand_amount_exact
    UNION ALL SELECT * FROM _cand_amount_tol
    UNION ALL SELECT * FROM _cand_invoice
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_iban
    UNION ALL SELECT * FROM _cand_bic
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

  ANALYZE _tx;
  ANALYZE _docs_use;
  ANALYZE _cand;

  SELECT COUNT(*) INTO v_candidates FROM _cand;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('candidates_rows', v_candidates)
      || jsonb_build_object('candidates_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  PERFORM public.reconciliations_log_step(
    p_run_id            => v_run_id,
    p_function_version  => p_athena_view,
    p_action            => 'candidates_built'::text,
    p_status            => 'ok'::text,
    p_target            => 'reconciliations'::text,
    p_message           => NULL::text,
    p_company_id        => p_company_id,
    p_transaction_id    => NULL::uuid,
    p_document_id       => NULL::uuid,
    p_reconciliation_id => NULL::uuid,
    p_rows_affected     => v_candidates::int,
    p_confidence        => NULL::numeric,
    p_step_ms           => NULL::bigint,
    p_processing_ms     => NULL::bigint,
    p_context           => jsonb_build_object('min_confidence', p_min_confidence)
  );

  -- resolve weights per candidate
  CREATE TEMP TABLE _rw AS
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant_raw, c.tx_merchant_raw) AS scope_merchant,
    COALESCE(rm.mer_date,     rc.comp_date,     rb.base_date)     AS date_weight,
    COALESCE(rm.mer_invoice,  rc.comp_invoice,  rb.base_invoice)  AS invoice_weight,
    COALESCE(rm.mer_order,    rc.comp_order,    rb.base_order)    AS order_weight,
    COALESCE(rm.mer_merchant, rc.comp_merchant, rb.base_merchant) AS merchant_weight,
    COALESCE(rm.mer_amount,   rc.comp_amount,   rb.base_amount)   AS amount_weight,
    COALESCE(rm.mer_currency, rc.comp_currency, rb.base_currency) AS currency_weight,
    COALESCE(rm.mer_customer, rc.comp_customer, rb.base_customer) AS customer_weight
  FROM _cand c
  CROSS JOIN _reg_base rb
  LEFT JOIN _reg_company rc ON rc.company_id = c.company_id_uuid
  LEFT JOIN _reg_merchant rm
         ON rm.company_id = c.company_id_uuid
        AND rm.merchant_id = COALESCE(c.doc_merchant_raw, c.tx_merchant_raw);

  CREATE INDEX ON _rw (transaction_id, document_id);

  -- flags + scores
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.order_id_norm   IS NOT NULL AND c.doc_order   = c.order_id_norm  ) AS order_match,
    (
      (
        c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL
        AND length(c.benef_core_ns) >= 3 AND length(c.merch_core_ns) >= 3
        AND (
             c.benef_core_ns = c.merch_core_ns
          OR position(c.merch_core_ns in c.benef_core_ns) > 0
          OR position(c.benef_core_ns in c.merch_core_ns) > 0
        )
      )
      OR (c.tx_iban_ns IS NOT NULL AND c.merch_iban_ns IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_iban_ns = c.merch_iban_ns)
      OR (c.tx_bic_ns  IS NOT NULL AND c.merch_bic_ns  IS NOT NULL
          AND c.amount_num > 0
          AND c.tx_bic_ns  = c.merch_bic_ns)
      OR (
           c.merch_core_ns IS NOT NULL AND length(c.merch_core_ns) >= 3
        AND c.info_tx_ns   IS NOT NULL
        AND position(c.merch_core_ns in c.info_tx_ns) > 0
      )
    ) AS merchant_match,
    (
      c.consumer_core_ns IS NOT NULL AND (
        (c.custinfo_core_ns IS NOT NULL AND
          ( c.consumer_core_ns = c.custinfo_core_ns
            OR position(c.consumer_core_ns in c.custinfo_core_ns) > 0
            OR position(c.custinfo_core_ns in c.consumer_core_ns) > 0 ))
        OR (c.info_ns IS NOT NULL AND position(c.consumer_core_ns in c.info_ns) > 0)
      )
    ) AS customer_match,
    abs(c.total_num - c.amount_num) AS amount_delta,
    CASE WHEN c.total_num IS NULL OR c.amount_num IS NULL THEN NULL
         ELSE abs(c.total_num - c.amount_num) / NULLIF(c.total_num,0)
    END AS amount_delta_pct,
    (c.amount_num IS NOT NULL AND c.total_num IS NOT NULL
       AND abs(c.total_num - c.amount_num) <= GREATEST(p_amount_tol_floor, c.total_num * p_amount_tol_pct)) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

