  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,
    s.tx_is_in, s.tx_is_out,

    (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.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.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score_raw,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score_raw,

    (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
          ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END)
     END) AS merchant_score,
    (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
          ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END)
     END) AS customer_score,

    ((s.tx_is_in AND s.customer_match) OR (s.tx_is_out AND s.merchant_match)) AS direction_ok,

    (
      (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.currency_match THEN rw.currency_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.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
            ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
      (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
            ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) 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.currency_match THEN rw.currency_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.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
                    ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
              (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
                    ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) 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, c.direction_ok
    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,
             c.direction_ok 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.direction_ok 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.direction_ok DESC,
                        b.amount_delta ASC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use (
       company_id_txt, company_id_uuid, transaction_id, document_id,
       confidence, numerator, total_weight, currency_weight, date_diff_days,
       fiscal_year, quarter, month_text, amount_delta, direction_ok, rn
    )
    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, amount_delta, direction_ok, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based)
  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, 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
        ),
        'direction', jsonb_build_object('in', tx.tx_is_in, 'out', tx.tx_is_out)
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    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;

    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,
        'direction_ok', c.direction_ok,
        '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;

    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, 'direction_ok', c.direction_ok)
    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;

    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,
        'direction_ok', c.direction_ok,
        '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_v29(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

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

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

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

create or replace function athena_reconcile_general_v30(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v30'::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;

  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,
    s.tx_is_in, s.tx_is_out,

    (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.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.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score_raw,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score_raw,

    (CASE WHEN s.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
          ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END)
     END) AS merchant_score,
    (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
          ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END)
     END) AS customer_score,

    ((s.tx_is_in AND s.customer_match) OR (s.tx_is_out AND s.merchant_match)) AS direction_ok,

    (
      (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.currency_match THEN rw.currency_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.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
            ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
      (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
            ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) 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.currency_match THEN rw.currency_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.tx_is_out THEN (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END)
                    ELSE (CASE WHEN s.merchant_match THEN 0.5 * rw.merchant_weight ELSE 0 END) END) +
              (CASE WHEN s.tx_is_in THEN (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
                    ELSE (CASE WHEN s.customer_match THEN 0.5 * rw.customer_weight ELSE 0 END) 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, c.direction_ok
    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,
             c.direction_ok 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.direction_ok 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.direction_ok DESC,
                        b.amount_delta ASC,
                        b.date_diff_days ASC,
                        b.transaction_id
             ) AS doc_rn
      FROM _best b
    )
    INSERT INTO _best_use (
       company_id_txt, company_id_uuid, transaction_id, document_id,
       confidence, numerator, total_weight, currency_weight, date_diff_days,
       fiscal_year, quarter, month_text, amount_delta, direction_ok, rn
    )
    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, amount_delta, direction_ok, rn
    FROM pick_one
    WHERE doc_rn = 1;
  END IF;

  -- activity logging (set-based)
  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, 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
        ),
        'direction', jsonb_build_object('in', tx.tx_is_in, 'out', tx.tx_is_out)
      )
    FROM _tx tx
    LEFT JOIN _cand c ON c.transaction_id = tx.transaction_id
    WHERE c.transaction_id IS NULL;

    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;

    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,
        'direction_ok', c.direction_ok,
        '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;

    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, 'direction_ok', c.direction_ok)
    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;

    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)
  );

  -- write match scores (rounded to 2 decimals)
  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,
    ROUND(c.confidence, 2),
    ROUND(c.numerator, 2),
    ROUND(c.total_weight, 2),
    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,
    ROUND(c.date_score, 2), ROUND(c.invoice_score, 2), ROUND(c.order_score, 2),
    ROUND(c.merchant_score, 2), ROUND(c.amount_score, 2), ROUND(c.currency_score, 2), ROUND(c.customer_score, 2)
  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,
        'direction_ok', c.direction_ok,
        '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_v30(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

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

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

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

create or replace function athena_reconcile_general_v1_1(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
  RAISE NOTICE 'step: % ms', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)));
  -- 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_v1_1(unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown) owner to postgres;

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

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

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

create or replace function get_schema_json()
    returns TABLE(schema jsonb)
    stable
    language sql
as
$$
  select jsonb_agg(
    jsonb_build_object(
      'table_schema', n.nspname,
      'table_name', c.relname,
      'columns', (
        select jsonb_agg(
          jsonb_build_object(
            'column_name', a.attname,
            'data_type', format_type(a.atttypid, a.atttypmod),
            'default', pg_get_expr(d.adbin, d.adrelid)
          ) order by a.attnum
        )
        from pg_attribute a
        join pg_type t on a.atttypid = t.oid
        left join pg_attrdef d on a.attrelid = d.adrelid and a.attnum = d.adnum
        where a.attrelid = c.oid
          and a.attnum > 0
          and not a.attisdropped
      )
    )
  ) as schema
  from pg_class c
  join pg_namespace n on n.oid = c.relnamespace
  where c.relkind = 'r'
    and n.nspname not in ('pg_catalog', 'information_schema');
$$;

alter function get_schema_json() owner to postgres;

grant execute on function get_schema_json() to anon;

grant execute on function get_schema_json() to authenticated;

grant execute on function get_schema_json() to service_role;

create or replace function athena_reconcile_general_v1_1(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  status_text text := 'ok';
  message_text text := 'success';

  -- engine/versioning
  engine_version text := '1.1';

  -- timing
  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean;
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- logging batch
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time numeric := EXTRACT(EPOCH FROM now())::bigint;
BEGIN
  /* 1) load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction := CASE WHEN config_amount_tolerance >= 1
                              THEN config_amount_tolerance / 100.0
                              ELSE config_amount_tolerance END;
  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0;
  END IF;

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

  SELECT
    COALESCE((weights_json->>'amount_total')::numeric,0)
  + COALESCE((weights_json->>'invoice_number')::numeric,0)
  + COALESCE((weights_json->>'order_number')::numeric,0)
  + COALESCE((weights_json->>'date')::numeric,0)
  + COALESCE((weights_json->>'currency')::numeric,0)
  + COALESCE((weights_json->>'merchant_id')::numeric,0)
  + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_max_possible;

  /* 3) pipeline + logging + inserts */
  WITH
  document_scope AS (
    SELECT document_id, company_id, total, currency, invoice_number, order_id,
           document_date_unix, merchant_id, customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND COALESCE(d.reconciled,false) = false
      AND NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.document_id = d.document_id)
  ),
  transactions_scope AS (
    SELECT transaction_id, company_id, amount, debited, credited, currency, invoice_nr, order_id,
           completed_date, merchant_id, name_consumer
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND COALESCE(t.reconciled,false) = false
      AND NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.transaction_id = t.transaction_id)
      AND (
           invoice_nr IS NOT NULL
        OR order_id   IS NOT NULL
        OR (currency IS NOT NULL AND merchant_id   IS NOT NULL)
        OR (currency IS NOT NULL AND name_consumer IS NOT NULL)
        OR (currency IS NOT NULL AND (amount IS NOT NULL OR debited IS NOT NULL OR credited IS NOT NULL))
      )
    ORDER BY completed_date DESC, transaction_id
    LIMIT derived_max_transactions
  ),

  candidate_pairs AS (
    SELECT t.transaction_id AS transaction_id, d.document_id AS document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (d.invoice_number IS NOT NULL AND d.invoice_number <> '' AND t.invoice_nr IS NOT NULL AND t.invoice_nr <> '' AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id IS NOT NULL AND d.order_id <> '' AND t.order_id IS NOT NULL AND t.order_id <> '' AND d.order_id = t.order_id)
          OR
          (d.currency = t.currency AND d.merchant_id = t.merchant_id)
          OR
          (d.currency = t.currency AND d.customer_name = t.name_consumer)
          OR
          (d.currency = t.currency AND (
             d.total = t.debited OR d.total = t.credited OR d.total = ABS(t.amount) OR d.total = t.amount
          ))
          OR
          (d.currency = t.currency AND d.total IS NOT NULL
            AND (
              CASE
                WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN ABS(d.total - t.debited)
                WHEN t.credited IS NOT NULL AND t.credited > 0 THEN ABS(d.total - t.credited)
                WHEN t.amount   IS NOT NULL                  THEN ABS(d.total - ABS(t.amount))
                ELSE NULL
              END
            ) IS NOT NULL
            AND (
              CASE
                WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN ABS(d.total - t.debited)
                WHEN t.credited IS NOT NULL AND t.credited > 0 THEN ABS(d.total - t.credited)
                WHEN t.amount   IS NOT NULL                  THEN ABS(d.total - ABS(t.amount))
                ELSE 999999999
              END
            ) <= d.total * derived_amount_tolerance_fraction
          )
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND d.invoice_number <> '' AND t.invoice_nr IS NOT NULL AND t.invoice_nr <> '' AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id IS NOT NULL AND d.order_id <> '' AND t.order_id IS NOT NULL AND t.order_id <> '' AND d.order_id = t.order_id THEN 1
          WHEN d.currency = t.currency AND (
             d.total = t.debited OR d.total = t.credited OR d.total = ABS(t.amount) OR d.total = t.amount
          ) THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
  ),
  distinct_pairs AS (
    SELECT DISTINCT cp.transaction_id, cp.document_id FROM candidate_pairs cp
  ),

  attributes AS (
    SELECT
      p.transaction_id, p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount, t.debited AS transaction_debited, t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency, d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix, d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id, d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id, a.document_id,

      CASE
        WHEN a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 THEN a.transaction_debited
        WHEN a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 THEN a.transaction_credited
        WHEN a.transaction_amount   IS NOT NULL                              THEN ABS(a.transaction_amount)
        ELSE NULL
      END AS transaction_effective_amount,

      COALESCE(
        (a.transaction_invoice_number IS NOT NULL AND a.transaction_invoice_number <> ''
         AND a.document_invoice_number IS NOT NULL AND a.document_invoice_number <> ''
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.transaction_order_id <> ''
         AND a.document_order_id IS NOT NULL AND a.document_order_id <> ''
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      e.invoice_number_match, e.order_number_match, e.date_match, e.currency_match, e.merchant_match, e.customer_match,
      e.transaction_effective_amount, e.document_total, e.transaction_date_unix, e.document_date_unix,
      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,
      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)))
              )
            )
          ELSE 0
        END
      ,0)::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,

      w.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::numeric AS customer_score,

      m.transaction_effective_amount, m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_confidence AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id, s.transaction_date_unix, s.document_date_unix, s.amount_direction_ok,
      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,
      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,
      CASE
        WHEN weights_max_possible > 0
          THEN ROUND( ((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                        / weights_max_possible) * 100.0 , 4)
        ELSE 0
      END AS confidence_percent,

      s.amount_match, s.invoice_match, s.order_match, s.date_match, s.currency_match, s.merchant_match, s.customer_match,
      s.date_weight, s.invoice_weight, s.order_weight, s.merchant_weight, s.amount_weight, s.currency_weight, s.customer_weight,
      s.date_score,  s.invoice_score,  s.order_score,  s.merchant_score,  s.amount_score,  s.currency_score,  s.customer_score,

      s.transaction_effective_amount, s.document_total
    FROM scored_components s
  ),

  -- thresholds
  low_signal AS (
    SELECT * FROM scored_with_confidence WHERE signals_matched_count < 1
  ),
  below_min_confidence AS (
    SELECT * FROM scored_with_confidence WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),

  -- deterministic top-1 per transaction
  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  not_top_for_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction > 1
  ),
  top_per_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1
  ),

  -- deterministic top-1 per document
  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  not_top_for_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document > 1
  ),
  top_per_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document = 1
  ),

  final_candidates AS (
    SELECT * FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),

  -- block if transaction OR document already present in reconciliations (any past run)
  blocked_existing AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       OR EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),
  allowed_for_insert AS (
    SELECT f.* FROM final_candidates f
    WHERE NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
      AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id,
      document_id,
      company_id,
      confidence,
      time,            -- unix time (numeric)
      engine_version,  -- engine version tag
      fiscal_year,     -- numeric
      quarter,         -- numeric 1-4
      month,           -- text month name
      summary          -- TEXT to preserve key order
    )
    WITH ts_base AS (
      SELECT
        f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        -- from final_candidates d (pick only what you need)
        d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_match,
        d.order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        d.transaction_effective_amount,
        d.document_total,
        d.transaction_date_unix,
        d.document_date_unix,

        -- derived timestamp base
        t.base_unix,

        -- from attributes a (tx/doc raw fields for summary)
        a.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d
        ON d.transaction_id = t.transaction_id
      AND d.document_id    = t.document_id
      JOIN attributes a
        ON a.transaction_id = d.transaction_id
      AND a.document_id    = d.document_id
    )
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.confidence_percent,
      log_run_unix_time,
      engine_version,
      EXTRACT(YEAR    FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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
      r.reconciliation_id,
      r.confidence,
      d.total_numerator,
      weights_max_possible,
      d.date_match, d.invoice_match, d.order_match, d.merchant_match, (d.amount_match)::bool, d.currency_match, d.customer_match,
      d.date_weight, d.invoice_weight, d.order_weight, d.merchant_weight, d.amount_weight, d.currency_weight, d.customer_weight,
      d.date_score,  d.invoice_score,  d.order_score,  d.merchant_score,  d.amount_score,  d.currency_score,  d.customer_score
    FROM inserted_reconciliations r
    JOIN final_candidates d
      ON d.transaction_id  = r.transaction_id
     AND d.document_id     = r.document_id
    RETURNING 1
  ),

  /* logging */
  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent,

      s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,

      s.amount_score, s.invoice_score, s.order_score, s.date_score,
      s.currency_score, s.merchant_score, s.customer_score,

      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight,
      s.currency_weight, s.merchant_weight, s.customer_weight,

      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  logging_matched_rows AS (
    SELECT
      i.reconciliation_id,
      log_run_id        AS run_id,
      log_run_unix_time AS time_unix,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text AS action,
      NULL::text      AS reason,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date',      lb.date_match,
          'order',     lb.order_match,
          'amount',    lb.amount_match,
          'invoice',   lb.invoice_match,
          'currency',  lb.currency_match,
          'customer',  lb.customer_match,
          'merchant',  lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount',   lb.amount_score,
          'invoice',  lb.invoice_score,
          'order',    lb.order_score,
          'date',     lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount',   lb.amount_weight,
          'invoice',  lb.invoice_weight,
          'order',    lb.order_weight,
          'date',     lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence',    lb.confidence_percent,
        'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                               THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                               ELSE NULL END,
        'direction_ok',  lb.amount_direction_ok,
        'date_diff_days',lb.date_diff_days
      ) AS details
    FROM inserted_reconciliations i
    JOIN logging_base lb ON lb.transaction_id = i.transaction_id AND lb.document_id = i.document_id
  ),

  logging_skipped_union AS (
    SELECT log_run_id AS run_id, log_run_unix_time AS time_unix, lb.company_id, x.transaction_id, x.document_id,
           'skipped'::text AS action, 'not_top_for_tx'::text AS reason,
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           ) AS details
    FROM not_top_for_transaction x JOIN logging_base lb USING (transaction_id, document_id)
    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_doc',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           ) AS details
    FROM not_top_for_document x JOIN logging_base lb USING (transaction_id, document_id)
    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','below_min_confidence',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency',  lb.currency_weight,
               'merchant',  lb.merchant_weight,
               'customer',  lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           ) AS details
    FROM below_min_confidence x JOIN logging_base lb USING (transaction_id, document_id)
    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','no_signals',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.amount_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           ) AS details
    FROM low_signal x JOIN logging_base lb USING (transaction_id, document_id)
    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','blocked_existing_tx_or_doc',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           ) AS details
    FROM blocked_existing x JOIN logging_base lb USING (transaction_id, document_id)
  ),

  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)  AS top_per_transaction_count,
      (SELECT COUNT(*) FROM top_per_document)     AS top_per_document_count,
      (SELECT COUNT(*) FROM final_candidates)     AS passed_confidence_count
  )
  SELECT top_per_transaction_count, top_per_document_count, passed_confidence_count
  INTO   summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count
  FROM final_counts;

  -- timing
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  RETURN jsonb_build_object(
    'msg',               message_text,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_2(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.2';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time numeric := EXTRACT(EPOCH FROM now())::bigint;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_2'),
                     COALESCE(hashtext(p_company_id::text), 0)
                   );
  IF NOT lock_acquired THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','another run in progress','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1) load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0;
  END IF;

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

  SELECT
    COALESCE((weights_json->>'amount_total')::numeric,0)
  + COALESCE((weights_json->>'invoice_number')::numeric,0)
  + COALESCE((weights_json->>'order_number')::numeric,0)
  + COALESCE((weights_json->>'date')::numeric,0)
  + COALESCE((weights_json->>'currency')::numeric,0)
  + COALESCE((weights_json->>'merchant_id')::numeric,0)
  + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_max_possible;

  /* 3) pipeline + inline logging */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND t.name_consumer IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT derived_max_transactions
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          OR
          (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR
          (d.currency = t.currency AND d.customer_name = t.name_consumer)
          OR
          (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
  ),

  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id FROM candidate_pairs
  ),

  attributes AS (
    SELECT
      p.transaction_id, p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount, t.debited AS transaction_debited, t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency, d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix, d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id, d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id, a.document_id,

      a.transaction_effective_amount,

      COALESCE(
        (a.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      e.invoice_number_match, e.order_number_match, e.date_match, e.currency_match, e.merchant_match, e.customer_match,
      e.transaction_effective_amount, e.document_total, e.transaction_date_unix, e.document_date_unix,
      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,
      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)))
              )
            )
          ELSE 0
        END
      ,0)::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,

      w.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::numeric AS customer_score,

      m.transaction_effective_amount, m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_confidence AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id, s.transaction_date_unix, s.document_date_unix, s.amount_direction_ok,
      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,
      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,
      CASE
        WHEN weights_max_possible > 0
          THEN ROUND( ((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                        / weights_max_possible) * 100.0 , 4)
        ELSE 0
      END AS confidence_percent,

      s.amount_match, s.invoice_match, s.order_match, s.date_match, s.currency_match, s.merchant_match, s.customer_match,
      s.date_weight, s.invoice_weight, s.order_weight, s.merchant_weight, s.amount_weight, s.currency_weight, s.customer_weight,
      s.date_score,  s.invoice_score,  s.order_score,  s.merchant_score,  s.amount_score,  s.currency_score,  s.customer_score,

      s.transaction_effective_amount, s.document_total
    FROM scored_components s
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  top_per_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1
  ),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  top_per_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document = 1
  ),

  final_candidates AS (
    SELECT * FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),

  /* single membership gate */
  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id,
      document_id,
      company_id,
      confidence,
      time,
      engine_version,
      fiscal_year,
      quarter,
      month,
      summary
    )
    WITH ts_base AS (
      SELECT
        f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_match,
        d.order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        d.transaction_effective_amount,
        d.document_total,
        d.transaction_date_unix,
        d.document_date_unix,
        t.base_unix,
        a.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d
        ON d.transaction_id = t.transaction_id
       AND d.document_id    = t.document_id
      JOIN attributes a
        ON a.transaction_id = d.transaction_id
       AND a.document_id    = d.document_id
    )
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.confidence_percent,
      log_run_unix_time,
      engine_version,
      EXTRACT(YEAR    FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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
      r.reconciliation_id,
      r.confidence,
      d.total_numerator,
      weights_max_possible,
      d.date_match, d.invoice_match, d.order_match, d.merchant_match, (d.amount_match)::bool, d.currency_match, d.customer_match,
      d.date_weight, d.invoice_weight, d.order_weight, d.merchant_weight, d.amount_weight, d.currency_weight, d.customer_weight,
      d.date_score,  d.invoice_score,  d.order_score,  d.merchant_score,  d.amount_score,  d.currency_score,  d.customer_score
    FROM inserted_reconciliations r
    JOIN final_candidates d
      ON d.transaction_id  = r.transaction_id
     AND d.document_id     = r.document_id
    RETURNING 1
  ),

  /* base for logs */
  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent,

      s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,

      s.amount_score, s.invoice_score, s.order_score, s.date_score,
      s.currency_score, s.merchant_score, s.customer_score,

      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight,
      s.currency_weight, s.merchant_weight, s.customer_weight,

      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  /* losers for logging */
  not_top_for_transaction AS (
    SELECT * FROM (
      SELECT
        b.*,
        ROW_NUMBER() OVER (
          PARTITION BY b.transaction_id
          ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
        ) AS rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity */
  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id,
      log_run_id        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date',      lb.date_match,
          'order',     lb.order_match,
          'amount',    lb.amount_match,
          'invoice',   lb.invoice_match,
          'currency',  lb.currency_match,
          'customer',  lb.customer_match,
          'merchant',  lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount',   lb.amount_score,
          'invoice',  lb.invoice_score,
          'order',    lb.order_score,
          'date',     lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount',   lb.amount_weight,
          'invoice',  lb.invoice_weight,
          'order',    lb.order_weight,
          'date',     lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence',    lb.confidence_percent,
        'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                               THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                               ELSE NULL END,
        'direction_ok',  lb.amount_direction_ok,
        'date_diff_days',lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* skipped -> reconciliations_activity */
  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_tx',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_transaction x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_doc',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_document x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','below_min_confidence',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','no_signals',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,  -- correct
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* final counts for return */
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)  AS top_per_transaction_count,
      (SELECT COUNT(*) FROM top_per_document)     AS top_per_document_count,
      (SELECT COUNT(*) FROM final_candidates)     AS passed_confidence_count
  )
  SELECT top_per_transaction_count, top_per_document_count, passed_confidence_count
  INTO   summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count
  FROM final_counts;

  -- timing
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  RETURN jsonb_build_object(
    'msg',               message_text,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_3(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.3';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;
  v_inserted_count bigint := 0;
  v_candidates_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time numeric := EXTRACT(EPOCH FROM now())::bigint;

  -- advisory lock
  lock_acquired boolean := true;

  -- error diagnostics
  err_state   text;
  err_msg     text;
  err_detail  text;
  err_hint    text;
  err_context text;
BEGIN
  /* single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_3'),
                     COALESCE(hashtext(p_company_id::text), 0)
                   );
  IF NOT lock_acquired THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','another run in progress','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0;
  END IF;

  /* resolve weights (normalized, company-first, safe fallback) */
  WITH candidate_weights AS (
    SELECT
      LOWER(TRIM(w.weight_key)) AS k_norm,
      (w.weight_value)::numeric AS v_norm,
      w.company_id
    FROM public.reconciliations_weight_registry w
    WHERE w.active
      AND w.merchant_id IS NULL
      AND (
           (p_company_id IS NULL AND w.company_id IS NULL)
        OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR w.company_id IS NULL))
      )
      AND LOWER(TRIM(w.weight_key)) IN (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked_weights AS (
    SELECT DISTINCT ON (k_norm)
           k_norm, v_norm
    FROM candidate_weights
    ORDER BY k_norm, (company_id IS NULL) -- FALSE(company) before TRUE(global)
  ),
  wj AS (
    SELECT jsonb_object_agg(k_norm, v_norm) AS j
    FROM picked_weights
  )
  SELECT COALESCE(j, '{}'::jsonb) INTO weights_json FROM wj;

  SELECT
      COALESCE((weights_json->>'amount_total')::numeric,0)
    + COALESCE((weights_json->>'invoice_number')::numeric,0)
    + COALESCE((weights_json->>'order_number')::numeric,0)
    + COALESCE((weights_json->>'date')::numeric,0)
    + COALESCE((weights_json->>'currency')::numeric,0)
    + COALESCE((weights_json->>'merchant_id')::numeric,0)
    + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_max_possible;

  IF weights_max_possible = 0 THEN
    weights_json := jsonb_build_object(
      'amount_total', 1, 'invoice_number', 1, 'order_number', 1,
      'date', 1, 'currency', 1, 'merchant_id', 1, 'customer_name', 1
    );
    weights_max_possible := 7;
  END IF;

  /* run_start log including resolved weights */
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', config_log_activity,
        'allow_doc_dups', config_allow_document_duplicates,
        'amount_tol_pct', derived_amount_tolerance_fraction,
        'min_confidence', config_min_confidence,
        'date_window_days', config_date_window_days,
        'max_tx_seed', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      ),
      'weights_json', weights_json,
      'weights_max_possible', weights_max_possible
    )
  );

  /* ---------- main pipeline ---------- */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),
  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,      -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND t.name_consumer IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT derived_max_transactions
  ),
  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
          OR (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR (d.currency = t.currency AND d.customer_name = t.name_consumer)
          OR (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
  ),
  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id FROM candidate_pairs
  ),
  attributes AS (
    SELECT
      p.transaction_id, p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount, t.debited AS transaction_debited, t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency, d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix, d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id, d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),
  evaluated AS (
    SELECT
      a.transaction_id, a.document_id,
      a.transaction_effective_amount,
      COALESCE((a.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
                AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,
      COALESCE((a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
                AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,
      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,
      a.document_total, a.document_company_id, a.transaction_date_unix, a.document_date_unix,
      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,
      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),
  with_amount_tolerance AS (
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      e.invoice_number_match, e.order_number_match, e.date_match, e.currency_match, e.merchant_match, e.customer_match,
      e.transaction_effective_amount, e.document_total, e.transaction_date_unix, e.document_date_unix,
      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,
      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),
  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),
  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,
      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total)
                                        / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)))
              )
            )
          ELSE 0
        END
      ,0)::numeric AS amount_score,
      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,
      w.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,
      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::numeric AS customer_score,
      m.transaction_effective_amount, m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),
  scored_with_confidence AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id, s.transaction_date_unix, s.document_date_unix, s.amount_direction_ok,
      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,
      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,
      CASE
        WHEN weights_max_possible > 0
          THEN ROUND( ((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                        / weights_max_possible) * 100.0 , 4)
        ELSE 0
      END AS confidence_percent,
      s.amount_match, s.invoice_match, s.order_match, s.date_match, s.currency_match, s.merchant_match, s.customer_match,
      s.date_weight, s.invoice_weight, s.order_weight, s.merchant_weight, s.amount_weight, s.currency_weight, s.customer_weight,
      s.date_score,  s.invoice_score,  s.order_score,  s.merchant_score,  s.amount_score,  s.currency_score,  s.customer_score,
      s.transaction_effective_amount, s.document_total
    FROM scored_components s
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),
  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  top_per_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1
  ),
  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  top_per_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document = 1
  ),
  final_candidates AS (
    SELECT * FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),
  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),
  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary
    )
    WITH ts_base AS (
      SELECT
        f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
        COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_match, d.order_match, d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        d.transaction_effective_amount, d.document_total, d.transaction_date_unix, d.document_date_unix,
        t.base_unix,
        a.transaction_invoice_number, a.document_invoice_number,
        a.transaction_order_id, a.document_order_id,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d ON d.transaction_id = t.transaction_id AND d.document_id = t.document_id
      JOIN attributes a       ON a.transaction_id = d.transaction_id AND a.document_id = d.document_id
    )
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      COALESCE(e.confidence_percent, 0)::numeric,
      log_run_unix_time,
      engine_version,
      EXTRACT(YEAR    FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),
  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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
      r.reconciliation_id, r.confidence, d.total_numerator, weights_max_possible,
      d.date_match, d.invoice_match, d.order_match, d.merchant_match, (d.amount_match)::bool, d.currency_match, d.customer_match,
      d.date_weight, d.invoice_weight, d.order_weight, d.merchant_weight, d.amount_weight, d.currency_weight, d.customer_weight,
      d.date_score,  d.invoice_score,  d.order_score,  d.merchant_score,  d.amount_score,  d.currency_score,  d.customer_score
    FROM inserted_reconciliations r
    JOIN final_candidates d ON d.transaction_id = r.transaction_id AND d.document_id = r.document_id
    RETURNING 1
  ),
  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id, s.confidence_percent,
      s.date_match, s.order_match, s.amount_match, s.invoice_match, s.currency_match, s.customer_match, s.merchant_match,
      s.amount_score, s.invoice_score, s.order_score, s.date_score, s.currency_score, s.merchant_score, s.customer_score,
      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight, s.currency_weight, s.merchant_weight, s.customer_weight,
      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),
  not_top_for_transaction AS (
    SELECT * FROM (
      SELECT b.*, ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT t.*, ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),
  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id, log_run_id, log_run_unix_time, lb.company_id, i.transaction_id, i.document_id,
      'matched'::text, NULL::text,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match,'order', lb.order_match,'amount', lb.amount_match,'invoice', lb.invoice_match,
          'currency', lb.currency_match,'customer', lb.customer_match,'merchant', lb.merchant_match),
        'scores', jsonb_build_object(
          'amount', lb.amount_score,'invoice', lb.invoice_score,'order', lb.order_score,'date', lb.date_score,
          'currency', lb.currency_score,'merchant', lb.merchant_score,'customer', lb.customer_score),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight,'invoice', lb.invoice_weight,'order', lb.order_weight,'date', lb.date_weight,
          'currency', lb.currency_weight,'merchant', lb.merchant_weight,'customer', lb.customer_weight),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4) ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),
  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_tx',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date', lb.date_match,'order', lb.order_match,'amount', lb.amount_match,'invoice', lb.invoice_match,
               'currency', lb.currency_match,'customer', lb.customer_match,'merchant', lb.merchant_match),
             'scores', jsonb_build_object(
               'amount', lb.amount_score,'invoice', lb.invoice_score,'order', lb.order_score,'date', lb.date_score,
               'currency', lb.currency_score,'merchant', lb.merchant_score,'customer', lb.customer_score),
             'weights', jsonb_build_object(
               'amount', lb.amount_weight,'invoice', lb.invoice_weight,'order', lb.order_weight,'date', lb.date_weight,
               'currency', lb.currency_weight,'merchant', lb.merchant_weight,'customer', lb.customer_weight),
             'confidence', lb.confidence_percent,
             'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                  THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4) ELSE NULL END,
             'direction_ok', lb.amount_direction_ok,
             'date_diff_days', lb.date_diff_days
           )
    FROM not_top_for_transaction x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_doc',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date', lb.date_match,'order', lb.order_match,'amount', lb.amount_match,'invoice', lb.invoice_match,
               'currency', lb.currency_match,'customer', lb.customer_match,'merchant', lb.merchant_match),
             'scores', jsonb_build_object(
               'amount', lb.amount_score,'invoice', lb.invoice_score,'order', lb.order_score,'date', lb.date_score,
               'currency', lb.currency_score,'merchant', lb.merchant_score,'customer', lb.customer_score),
             'weights', jsonb_build_object(
               'amount', lb.amount_weight,'invoice', lb.invoice_weight,'order', lb.order_weight,'date', lb.date_weight,
               'currency', lb.currency_weight,'merchant', lb.merchant_weight,'customer', lb.customer_weight),
             'confidence', lb.confidence_percent,
             'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                  THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4) ELSE NULL END,
             'direction_ok', lb.amount_direction_ok,
             'date_diff_days', lb.date_diff_days
           )
    FROM not_top_for_document x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','below_min_confidence',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date', lb.date_match,'order', lb.order_match,'amount', lb.amount_match,'invoice', lb.invoice_match,
               'currency', lb.currency_match,'customer', lb.customer_match,'merchant', lb.merchant_match),
             'scores', jsonb_build_object(
               'amount', lb.amount_score,'invoice', lb.invoice_score,'order', lb.order_score,'date', lb.date_score,
               'currency', lb.currency_score,'merchant', lb.merchant_score,'customer', lb.customer_score),
             'weights', jsonb_build_object(
               'amount', lb.amount_weight,'invoice', lb.invoice_weight,'order', lb.order_weight,'date', lb.date_weight,
               'currency', lb.currency_weight,'merchant', lb.merchant_weight,'customer', lb.customer_weight),
             'confidence', lb.confidence_percent,
             'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                  THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4) ELSE NULL END,
             'direction_ok', lb.amount_direction_ok,
             'date_diff_days', lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','no_signals',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date', lb.date_match,'order', lb.order_match,'amount', lb.amount_match,'invoice', lb.invoice_match,
               'currency', lb.currency_match,'customer', lb.customer_match,'merchant', lb.merchant_match),
             'scores', jsonb_build_object(
               'amount', lb.amount_score,'invoice', lb.invoice_score,'order', lb.order_score,'date', lb.date_score,
               'currency', lb.currency_score,'merchant', lb.merchant_score,'customer', lb.customer_score),
             'weights', jsonb_build_object(
               'amount', lb.amount_weight,'invoice', lb.invoice_weight,'order', lb.order_weight,'date', lb.date_weight,
               'currency', lb.currency_weight,'merchant', lb.merchant_weight,'customer', lb.customer_weight),
             'confidence', lb.confidence_percent,
             'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                  THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4) ELSE NULL END,
             'direction_ok', lb.amount_direction_ok,
             'date_diff_days', lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)      AS top_per_transaction_count,
      (SELECT COUNT(*) FROM top_per_document)         AS top_per_document_count,
      (SELECT COUNT(*) FROM final_candidates)         AS passed_confidence_count,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS inserted_count,
      (SELECT COUNT(*) FROM base_candidates)          AS candidates_count
  )
  SELECT
    top_per_transaction_count, top_per_document_count, passed_confidence_count,
    inserted_count, candidates_count
  INTO
    summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count,
    v_inserted_count, v_candidates_count
  FROM final_counts;

  /* timing */
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  /* run_end log */
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'top_per_tx', summary_top_per_transaction_count,
      'top_per_doc', summary_top_per_document_count,
      'passed_confidence', summary_passed_confidence_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               message_text,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );

EXCEPTION WHEN OTHERS THEN
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  GET STACKED DIAGNOSTICS
    err_state   = RETURNED_SQLSTATE,
    err_msg     = MESSAGE_TEXT,
    err_detail  = PG_EXCEPTION_DETAIL,
    err_hint    = PG_EXCEPTION_HINT,
    err_context = PG_EXCEPTION_CONTEXT;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_error', 'function', 'error', err_msg,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'sqlstate', err_state,
      'detail',   err_detail,
      'hint',     err_hint,
      'context',  err_context
    )
  );

  RETURN jsonb_build_object(
    'msg','error',
    'time_ms',elapsed_ms,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0,
    'error',err_msg,'sqlstate',err_state
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_5(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.5';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- for reconciliations_logs
  v_candidates_count bigint := 0;
  v_inserted_count bigint := 0;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  -- start log
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'reconciliation_config_id', p_reconciliation_config_id,
        'max_transactions', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      )
    )
  );

  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_3'),
                     COALESCE(hashtext(COALESCE(p_company_id::text,'__all__')), 0)
                   );
  IF NOT lock_acquired THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_end', 'function', 'ok', 'another run in progress',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms,
      jsonb_build_object('note','lock not acquired')
    );

    RETURN jsonb_build_object(
      'msg','another run in progress','time_ms',elapsed_ms,
      'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
    );
  END IF;

  /* 1) load configuration */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_end', 'function', 'error', 'reconciliations_config not found',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms, NULL
    );

    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0;
  END IF;

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

  SELECT
    COALESCE((weights_json->>'amount_total')::numeric,0)
  + COALESCE((weights_json->>'invoice_number')::numeric,0)
  + COALESCE((weights_json->>'order_number')::numeric,0)
  + COALESCE((weights_json->>'date')::numeric,0)
  + COALESCE((weights_json->>'currency')::numeric,0)
  + COALESCE((weights_json->>'merchant_id')::numeric,0)
  + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_max_possible;

  /* 3) pipeline + inserts + activity logs + pair cache updates */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND t.name_consumer IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT derived_max_transactions
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          OR
          (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR
          (d.currency = t.currency AND d.customer_name = t.name_consumer)
          OR
          (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
  ),

  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id FROM candidate_pairs
  ),

  attributes AS (
    SELECT
      p.transaction_id, p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount, t.debited AS transaction_debited, t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency, d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix, d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id, d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id, a.document_id,

      a.transaction_effective_amount,

      COALESCE(
        (a.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      e.invoice_number_match, e.order_number_match, e.date_match, e.currency_match, e.merchant_match, e.customer_match,
      e.transaction_effective_amount, e.document_total, e.transaction_date_unix, e.document_date_unix,
      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,
      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)))
              )
            )
          ELSE 0
        END
      ,0)::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,

      w.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::numeric AS customer_score,

      m.transaction_effective_amount, m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_confidence AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id, s.transaction_date_unix, s.document_date_unix, s.amount_direction_ok,
      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,
      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,
      CASE
        WHEN weights_max_possible > 0
          THEN ROUND( ((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                        / weights_max_possible) * 100.0 , 4)
        ELSE 0
      END AS confidence_percent,

      s.amount_match, s.invoice_match, s.order_match, s.date_match, s.currency_match, s.merchant_match, s.customer_match,
      s.date_weight, s.invoice_weight, s.order_weight, s.merchant_weight, s.amount_weight, s.currency_weight, s.customer_weight,
      s.date_score,  s.invoice_score,  s.order_score,  s.merchant_score,  s.amount_score,  s.currency_score,  s.customer_score,

      s.transaction_effective_amount, s.document_total
    FROM scored_components s
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  top_per_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1
  ),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  top_per_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document = 1
  ),

  final_candidates AS (
    SELECT * FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),

  /* single membership gate */
  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id,
      document_id,
      company_id,
      confidence,
      time,
      engine_version,
      fiscal_year,
      quarter,
      month,
      summary
    )
    WITH ts_base AS (
      SELECT
        f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_match,
        d.order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        d.transaction_effective_amount,
        d.document_total,
        d.transaction_date_unix,
        d.document_date_unix,
        t.base_unix,
        a.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d
        ON d.transaction_id = t.transaction_id
       AND d.document_id    = t.document_id
      JOIN attributes a
        ON a.transaction_id = d.transaction_id
       AND a.document_id    = d.document_id
    )
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.confidence_percent,
      log_run_unix_time,
      engine_version,
      EXTRACT(YEAR    FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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
      r.reconciliation_id,
      r.confidence,
      d.total_numerator,
      weights_max_possible,
      d.date_match, d.invoice_match, d.order_match, d.merchant_match, (d.amount_match)::bool, d.currency_match, d.customer_match,
      d.date_weight, d.invoice_weight, d.order_weight, d.merchant_weight, d.amount_weight, d.currency_weight, d.customer_weight,
      d.date_score,  d.invoice_score,  d.order_score,  d.merchant_score,  d.amount_score,  d.currency_score,  d.customer_score
    FROM inserted_reconciliations r
    JOIN final_candidates d
      ON d.transaction_id  = r.transaction_id
     AND d.document_id     = r.document_id
    RETURNING 1
  ),

  /* base for activity logs */
  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent,

      s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,

      s.amount_score, s.invoice_score, s.order_score, s.date_score,
      s.currency_score, s.merchant_score, s.customer_score,

      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight,
      s.currency_weight, s.merchant_weight, s.customer_weight,

      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  /* losers for logging and cache */
  not_top_for_transaction AS (
    SELECT * FROM (
      SELECT
        b.*,
        ROW_NUMBER() OVER (
          PARTITION BY b.transaction_id
          ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
        ) AS rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity (if enabled) */
  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id,
      log_run_id        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date',      lb.date_match,
          'order',     lb.order_match,
          'amount',    lb.amount_match,
          'invoice',   lb.invoice_match,
          'currency',  lb.currency_match,
          'customer',  lb.customer_match,
          'merchant',  lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount',   lb.amount_score,
          'invoice',  lb.invoice_score,
          'order',    lb.order_score,
          'date',     lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount',   lb.amount_weight,
          'invoice',  lb.invoice_weight,
          'order',    lb.order_weight,
          'date',     lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence',    lb.confidence_percent,
        'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                               THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                               ELSE NULL END,
        'direction_ok',  lb.amount_direction_ok,
        'date_diff_days',lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* skipped -> reconciliations_activity (if enabled) */
  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_tx',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_transaction x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_doc',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_document x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','below_min_confidence',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','no_signals',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* pair cache: matched */
  cache_upsert_matched AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT
      lb.company_id, i.transaction_id, i.document_id,
      'matched', lb.confidence_percent, log_run_unix_time
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* pair cache: skipped */
  cache_upsert_skipped AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* final counts for return and logging */
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)  AS c_top_per_transaction,
      (SELECT COUNT(*) FROM top_per_document)     AS c_top_per_document,
      (SELECT COUNT(*) FROM final_candidates)     AS c_passed_confidence,
      (SELECT COUNT(*) FROM base_candidates)      AS c_base_candidates,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS c_inserted
  )
  SELECT
    c_top_per_transaction, c_top_per_document, c_passed_confidence,
    c_base_candidates, c_inserted
  INTO
    summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count,
    v_candidates_count, v_inserted_count
  FROM final_counts;

  -- end log
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'inserted',  v_inserted_count,
      'candidates', v_candidates_count,
      'top_per_tx', summary_top_per_transaction_count,
      'top_per_doc', summary_top_per_document_count,
      'passed_confidence', summary_passed_confidence_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               message_text,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );

EXCEPTION WHEN OTHERS THEN
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  BEGIN
    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_error', 'function', 'error', SQLERRM,
      p_company_id, NULL, NULL, NULL,
      NULL, NULL, NULL, elapsed_ms,
      jsonb_build_object('hint','see server logs or activity tables')
    );
  EXCEPTION WHEN OTHERS THEN
    -- swallow logging failure
    NULL;
  END;

  RETURN jsonb_build_object(
    'msg','error',
    'error', SQLERRM,
    'time_ms', elapsed_ms,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_6(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.6';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;
  config_use_pair_cache boolean := false;   -- NEW

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- for reconciliations_logs
  v_candidates_count bigint := 0;
  v_inserted_count bigint := 0;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  /* run_start log */
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'reconciliation_config_id', p_reconciliation_config_id,
        'max_transactions', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      )
    )
  );

  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_6'),
                     COALESCE(hashtext(COALESCE(p_company_id::text,'__all__')), 0)
                   );
  IF NOT lock_acquired THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_end', 'function', 'ok', 'another run in progress',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms,
      jsonb_build_object('note','lock not acquired')
    );

    RETURN jsonb_build_object(
      'msg','another run in progress','time_ms',elapsed_ms,
      'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
    );
  END IF;

  /* 1) load configuration (includes use_pair_cache) */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity, COALESCE(use_pair_cache,false)
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity, config_use_pair_cache
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_end', 'function', 'error', 'reconciliations_config not found',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms, NULL
    );

    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0;
  END IF;

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

  SELECT
    COALESCE((weights_json->>'amount_total')::numeric,0)
  + COALESCE((weights_json->>'invoice_number')::numeric,0)
  + COALESCE((weights_json->>'order_number')::numeric,0)
  + COALESCE((weights_json->>'date')::numeric,0)
  + COALESCE((weights_json->>'currency')::numeric,0)
  + COALESCE((weights_json->>'merchant_id')::numeric,0)
  + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_max_possible;

  /* 3) pipeline + inserts + activity logs + pair cache updates */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND t.name_consumer IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT derived_max_transactions
  ),

  /* cache: known-good matched pairs, enabled only if config_use_pair_cache */
  cache_pairs AS (
    SELECT c.transaction_id, c.document_id
    FROM public.reconciliations_pair_cache_id c
    WHERE config_use_pair_cache
      AND (p_company_id IS NULL OR c.company_id = p_company_id)
      AND c.last_action = 'matched'
  ),

  seeded_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN cache_pairs c ON c.transaction_id = t.transaction_id
    JOIN document_scope d ON d.document_id = c.document_id
    WHERE NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.transaction_id = t.transaction_id)
      AND NOT EXISTS (SELECT 1 FROM public.reconciliations r WHERE r.document_id    = d.document_id)
  ),

  candidate_pairs AS (
    SELECT * FROM seeded_pairs
    UNION ALL
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          -- if cache disabled, this clause is neutral;
          -- if enabled, it drops known-bad pairs
          (NOT config_use_pair_cache) OR NOT EXISTS (
            SELECT 1
            FROM public.reconciliations_pair_cache_id c
            WHERE c.transaction_id = t.transaction_id
              AND c.document_id    = d.document_id
              AND c.last_action    = 'skipped'
          )
        )
        AND (
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          OR
          (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR
          (d.currency = t.currency AND d.customer_name = t.name_consumer)
          OR
          (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
  ),

  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id FROM candidate_pairs
  ),

  attributes AS (
    SELECT
      p.transaction_id, p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount, t.debited AS transaction_debited, t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency, d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix, d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id, d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id, a.document_id,

      a.transaction_effective_amount,

      COALESCE(
        (a.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      e.invoice_number_match, e.order_number_match, e.date_match, e.currency_match, e.merchant_match, e.customer_match,
      e.transaction_effective_amount, e.document_total, e.transaction_date_unix, e.document_date_unix,
      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,
      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)))
              )
            )
          ELSE 0
        END
      ,0)::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,

      w.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::numeric AS customer_score,

      m.transaction_effective_amount, m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_confidence AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id, s.transaction_date_unix, s.document_date_unix, s.amount_direction_ok,
      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,
      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,
      CASE
        WHEN weights_max_possible > 0
          THEN ROUND( ((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                        / weights_max_possible) * 100.0 , 4)
        ELSE 0
      END AS confidence_percent,

      s.amount_match, s.invoice_match, s.order_match, s.date_match, s.currency_match, s.merchant_match, s.customer_match,
      s.date_weight, s.invoice_weight, s.order_weight, s.merchant_weight, s.amount_weight, s.currency_weight, s.customer_weight,
      s.date_score,  s.invoice_score,  s.order_score,  s.merchant_score,  s.amount_score,  s.currency_score,  s.customer_score,

      s.transaction_effective_amount, s.document_total
    FROM scored_components s
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  top_per_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1
  ),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  top_per_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document = 1
  ),

  final_candidates AS (
    SELECT * FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),

  /* single membership gate */
  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id,
      document_id,
      company_id,
      confidence,
      time,
      engine_version,
      fiscal_year,
      quarter,
      month,
      summary
    )
    WITH ts_base AS (
      SELECT
        f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_match,
        d.order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        d.transaction_effective_amount,
        d.document_total,
        d.transaction_date_unix,
        d.document_date_unix,
        t.base_unix,
        a.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d
        ON d.transaction_id = t.transaction_id
       AND d.document_id    = t.document_id
      JOIN attributes a
        ON a.transaction_id = d.transaction_id
       AND a.document_id    = d.document_id
    )
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.confidence_percent,
      log_run_unix_time,
      engine_version,
      EXTRACT(YEAR    FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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
      r.reconciliation_id,
      r.confidence,
      d.total_numerator,
      weights_max_possible,
      d.date_match, d.invoice_match, d.order_match, d.merchant_match, (d.amount_match)::bool, d.currency_match, d.customer_match,
      d.date_weight, d.invoice_weight, d.order_weight, d.merchant_weight, d.amount_weight, d.currency_weight, d.customer_weight,
      d.date_score,  d.invoice_score,  d.order_score,  d.merchant_score,  d.amount_score,  d.currency_score,  d.customer_score
    FROM inserted_reconciliations r
    JOIN final_candidates d
      ON d.transaction_id  = r.transaction_id
     AND d.document_id     = r.document_id
    RETURNING 1
  ),

  /* base for activity logs */
  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent,

      s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,

      s.amount_score, s.invoice_score, s.order_score, s.date_score,
      s.currency_score, s.merchant_score, s.customer_score,

      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight,
      s.currency_weight, s.merchant_weight, s.customer_weight,

      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  /* losers for logging and cache */
  not_top_for_transaction AS (
    SELECT * FROM (
      SELECT
        b.*,
        ROW_NUMBER() OVER (
          PARTITION BY b.transaction_id
          ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
        ) AS rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity (if enabled) */
  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id,
      log_run_id        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date',      lb.date_match,
          'order',     lb.order_match,
          'amount',    lb.amount_match,
          'invoice',   lb.invoice_match,
          'currency',  lb.currency_match,
          'customer',  lb.customer_match,
          'merchant',  lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount',   lb.amount_score,
          'invoice',  lb.invoice_score,
          'order',    lb.order_score,
          'date',     lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount',   lb.amount_weight,
          'invoice',  lb.invoice_weight,
          'order',    lb.order_weight,
          'date',     lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence',    lb.confidence_percent,
        'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                               THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                               ELSE NULL END,
        'direction_ok',  lb.amount_direction_ok,
        'date_diff_days',lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* skipped -> reconciliations_activity (if enabled) */
  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_tx',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_transaction x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_doc',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_document x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','below_min_confidence',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant',  lb.merchant_weight,
               'customer',  lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','no_signals',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency', lb.currency_weight,
               'merchant', lb.merchant_weight,
               'customer', lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* pair cache: matched */
  cache_upsert_matched AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT
      lb.company_id, i.transaction_id, i.document_id,
      'matched', lb.confidence_percent, log_run_unix_time
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* pair cache: skipped */
  cache_upsert_skipped AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    UNION
    SELECT lb.company_id, x.transaction_id, x.document_id,
           'skipped', lb.confidence_percent, log_run_unix_time
    FROM (
      SELECT * FROM scored_with_confidence
      WHERE signals_matched_count < 1
    ) x
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* final counts for return and logging */
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)          AS c_top_per_transaction,
      (SELECT COUNT(*) FROM top_per_document)             AS c_top_per_document,
      (SELECT COUNT(*) FROM final_candidates)             AS c_passed_confidence,
      (SELECT COUNT(*) FROM base_candidates)              AS c_base_candidates,
      (SELECT COUNT(*) FROM inserted_reconciliations)     AS c_inserted
  )
  SELECT
    c_top_per_transaction, c_top_per_document, c_passed_confidence,
    c_base_candidates, c_inserted
  INTO
    summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count,
    v_candidates_count, v_inserted_count
  FROM final_counts;

  /* run_end log */
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'inserted',  v_inserted_count,
      'candidates', v_candidates_count,
      'top_per_tx', summary_top_per_transaction_count,
      'top_per_doc', summary_top_per_document_count,
      'passed_confidence', summary_passed_confidence_count,
      'use_pair_cache', config_use_pair_cache
    )
  );

  RETURN jsonb_build_object(
    'msg',               message_text,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );

EXCEPTION WHEN OTHERS THEN
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  BEGIN
    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_error', 'function', 'error', SQLERRM,
      p_company_id, NULL, NULL, NULL,
      NULL, NULL, NULL, elapsed_ms,
      jsonb_build_object('hint','see server logs or activity tables')
    );
  EXCEPTION WHEN OTHERS THEN
    NULL;
  END;

  RETURN jsonb_build_object(
    'msg','error',
    'error', SQLERRM,
    'time_ms', elapsed_ms,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_7(p_company_id uuid, p_reconciliation_config_id uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  message_text text := 'success';
  engine_version text := '1.7';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- configuration
  config_amount_tolerance numeric;
  config_date_window_days numeric;
  config_min_confidence   numeric;
  config_allow_document_duplicates boolean; -- reserved
  config_log_activity boolean;
  config_use_pair_cache boolean := false;   -- toggle cache pruning

  -- derived configuration
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds bigint;
  derived_seed_cap_per_criterion int := 10;
  derived_max_transactions  int := 20000;

  -- weights
  weights_json jsonb := '{}'::jsonb;
  weights_max_possible  numeric := 0;

  -- summary counts
  summary_top_per_transaction_count bigint := 0;
  summary_top_per_document_count bigint := 0;
  summary_passed_confidence_count bigint := 0;

  -- run meta
  log_run_id uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- for reconciliations_logs
  v_candidates_count bigint := 0;
  v_inserted_count bigint := 0;

  -- advisory lock
  lock_acquired boolean := true;
BEGIN
  -- start log
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'reconciliation_config_id', p_reconciliation_config_id,
        'max_transactions', derived_max_transactions,
        'seed_cap_per_criterion', derived_seed_cap_per_criterion
      )
    )
  );

  /* 0) single-company run guard */
  lock_acquired := pg_try_advisory_xact_lock(
                     hashtext('athena_reconcile_general_v1_7'),
                     COALESCE(hashtext(COALESCE(p_company_id::text,'__all__')), 0)
                   );
  IF NOT lock_acquired THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_end', 'function', 'ok', 'another run in progress',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms,
      jsonb_build_object('note','lock not acquired')
    );

    RETURN jsonb_build_object(
      'msg','another run in progress','time_ms',elapsed_ms,
      'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
    );
  END IF;

  /* 1) load configuration (includes use_pair_cache) */
  SELECT amount_tolerance, date_window_days, min_confidence,
         allow_doc_duplicates, log_activity, COALESCE(use_pair_cache,false)
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity, config_use_pair_cache
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_end', 'function', 'error', 'reconciliations_config not found',
      p_company_id, NULL, NULL, NULL,
      0, NULL, NULL, elapsed_ms, NULL
    );

    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  /* 1a) derive configuration */
  derived_amount_tolerance_fraction :=
    CASE WHEN config_amount_tolerance >= 1
         THEN config_amount_tolerance / 100.0
         ELSE config_amount_tolerance
    END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0;
  END IF;

  /* 2) resolve weights */
  weights_json :=
    COALESCE((
      WITH candidate_weights AS (
        SELECT w.*
        FROM public.reconciliations_weight_registry w
        WHERE w.active
          AND (
               (p_company_id IS NULL AND w.company_id IS NULL AND w.merchant_id IS NULL)
            OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR (w.company_id IS NULL AND w.merchant_id IS NULL)))
          )
      ),
      picked_weights AS (
        SELECT DISTINCT ON (weight_key) weight_key, weight_value, company_id
        FROM candidate_weights
        ORDER BY weight_key, CASE WHEN company_id IS NULL THEN 1 ELSE 0 END
      )
      SELECT jsonb_object_agg(weight_key, weight_value) FROM picked_weights
    ), '{}'::jsonb);

  SELECT
    COALESCE((weights_json->>'amount_total')::numeric,0)
  + COALESCE((weights_json->>'invoice_number')::numeric,0)
  + COALESCE((weights_json->>'order_number')::numeric,0)
  + COALESCE((weights_json->>'date')::numeric,0)
  + COALESCE((weights_json->>'currency')::numeric,0)
  + COALESCE((weights_json->>'merchant_id')::numeric,0)
  + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_max_possible;

  /* 3) pipeline + inserts + activity logs + pair cache (positive-only) */
  WITH
  document_scope AS (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS (
    SELECT
      t.transaction_id,
      t.company_id,                              -- text
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND t.name_consumer IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT derived_max_transactions
  ),

  tx_with_cache AS (
    SELECT DISTINCT t.transaction_id
    FROM transactions_scope t
    JOIN public.reconciliations_pair_cache_id rpc
      ON rpc.transaction_id = t.transaction_id
  ),

  candidate_pairs_cached AS (
    SELECT
      t.transaction_id,
      d.document_id
    FROM transactions_scope t
    JOIN tx_with_cache twc
      ON twc.transaction_id = t.transaction_id
    JOIN public.reconciliations_pair_cache_id rpc
      ON rpc.transaction_id = t.transaction_id
    JOIN document_scope d
      ON d.document_id = rpc.document_id
     AND d.company_id = t.company_id::uuid
    WHERE
      config_use_pair_cache
      AND (
           (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
        OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
        OR (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
        OR (d.currency = t.currency AND d.customer_name = t.name_consumer)
        OR (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
           ))
      )
  ),

  candidate_pairs_uncached AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR
          (d.order_id   IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id   = t.order_id)
          OR
          (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR
          (d.currency = t.currency AND d.customer_name = t.name_consumer)
          OR
          (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (derived_seed_cap_per_criterion * 6)
    ) d ON TRUE
    WHERE
      (NOT config_use_pair_cache)
      OR (config_use_pair_cache AND NOT EXISTS (
            SELECT 1 FROM tx_with_cache twc WHERE twc.transaction_id = t.transaction_id
          ))
  ),

  candidate_pairs AS (
    SELECT * FROM candidate_pairs_cached
    UNION ALL
    SELECT * FROM candidate_pairs_uncached
  ),

  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id FROM candidate_pairs
  ),

  attributes AS (
    SELECT
      p.transaction_id, p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount, t.debited AS transaction_debited, t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency, d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number, d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id, d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix, d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id, d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name, d.customer_name AS document_customer_name,
      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id, a.document_id,

      a.transaction_effective_amount,

      COALESCE(
        (a.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
         AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,

      COALESCE(
        (a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
         AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,

      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      e.invoice_number_match, e.order_number_match, e.date_match, e.currency_match, e.merchant_match, e.customer_match,
      e.transaction_effective_amount, e.document_total, e.transaction_date_unix, e.document_date_unix,
      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,
      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE ELSE FALSE
      END AS amount_tolerance_match,
      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS weight_amount,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS weight_invoice,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS weight_order,
      COALESCE((weights_json->>'date')::numeric, 0)           AS weight_date,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS weight_currency,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS weight_merchant,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS weight_customer
  ),

  scored_components AS (
    SELECT
      m.transaction_id, m.document_id, m.document_company_id, m.transaction_date_unix, m.document_date_unix, m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.weight_amount
          WHEN m.amount_tolerance_match THEN
            GREATEST(
              0,
              LEAST(
                w.weight_amount,
                w.weight_amount * (1 - (ABS(m.transaction_effective_amount - m.document_total) / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)))
              )
            )
          ELSE 0
        END
      ,0)::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,

      w.weight_amount   AS amount_weight,
      w.weight_invoice  AS invoice_weight,
      w.weight_order    AS order_weight,
      w.weight_date     AS date_weight,
      w.weight_currency AS currency_weight,
      w.weight_merchant AS merchant_weight,
      w.weight_customer AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.weight_invoice)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.weight_order)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.weight_date)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.weight_currency)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.weight_merchant)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.weight_customer)::numeric AS customer_score,

      m.transaction_effective_amount, m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_confidence AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id, s.transaction_date_unix, s.document_date_unix, s.amount_direction_ok,
      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,
      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,
      CASE
        WHEN weights_max_possible > 0
          THEN ROUND( ((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                        / weights_max_possible) * 100.0 , 4)
        ELSE 0
      END AS confidence_percent,

      s.amount_match, s.invoice_match, s.order_match, s.date_match, s.currency_match, s.merchant_match, s.customer_match,
      s.date_weight, s.invoice_weight, s.order_weight, s.merchant_weight, s.amount_weight, s.currency_weight, s.customer_weight,

      s.date_score,  s.invoice_score,  s.order_score,  s.merchant_score,  s.amount_score,  s.currency_score,  s.customer_score,

      s.transaction_effective_amount, s.document_total
    FROM scored_components s
  ),

  /* thresholds and ranking */
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  top_per_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1
  ),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  top_per_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document = 1
  ),

  final_candidates AS (
    SELECT * FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),

  /* single membership gate */
  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id,
      document_id,
      company_id,
      confidence,
      time,
      engine_version,
      fiscal_year,
      quarter,
      month,
      summary
    )
    WITH ts_base AS (
      SELECT
        f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_match,
        d.order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        d.transaction_effective_amount,
        d.document_total,
        d.transaction_date_unix,
        d.document_date_unix,
        t.base_unix,
        a.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d
        ON d.transaction_id = t.transaction_id
       AND d.document_id    = t.document_id
      JOIN attributes a
        ON a.transaction_id = d.transaction_id
       AND a.document_id    = d.document_id
    )
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      e.confidence_percent,
      log_run_unix_time,
      engine_version,
      EXTRACT(YEAR    FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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
      r.reconciliation_id,
      r.confidence,
      d.total_numerator,
      weights_max_possible,
      d.date_match, d.invoice_match, d.order_match, d.merchant_match, (d.amount_match)::bool, d.currency_match, d.customer_match,
      d.date_weight, d.invoice_weight, d.order_weight, d.merchant_weight, d.amount_weight, d.currency_weight, d.customer_weight,
      d.date_score,  d.invoice_score,  d.order_score,  d.merchant_score,  d.amount_score,  d.currency_score,  d.customer_score
    FROM inserted_reconciliations r
    JOIN final_candidates d
      ON d.transaction_id  = r.transaction_id
     AND d.document_id     = r.document_id
    RETURNING 1
  ),

  /* base for activity logs */
  logging_base AS (
    SELECT
      s.transaction_id, s.document_id, s.document_company_id AS company_id,
      s.confidence_percent,

      s.date_match, s.order_match, s.amount_match, s.invoice_match,
      s.currency_match, s.customer_match, s.merchant_match,

      s.amount_score, s.invoice_score, s.order_score, s.date_score,
      s.currency_score, s.merchant_score, s.customer_score,

      s.amount_weight, s.invoice_weight, s.order_weight, s.date_weight,
      s.currency_weight, s.merchant_weight, s.customer_weight,

      s.transaction_effective_amount, s.document_total, s.amount_direction_ok,
      CASE WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL
           THEN (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
           ELSE NULL::numeric END AS date_diff_days
    FROM scored_with_confidence s
  ),

  /* losers for logging */
  not_top_for_transaction AS (
    SELECT * FROM (
      SELECT
        b.*,
        ROW_NUMBER() OVER (
          PARTITION BY b.transaction_id
          ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
        ) AS rn
      FROM base_candidates b
    ) q WHERE rn > 1
  ),
  not_top_for_document AS (
    SELECT * FROM (
      SELECT
        t.*,
        ROW_NUMBER() OVER (
          PARTITION BY t.document_id
          ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
        ) AS rn
      FROM top_per_transaction t
    ) q WHERE rn > 1
  ),

  /* matched -> reconciliation_activity (if enabled) */
  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id,
      log_run_id        AS run_id,
      log_run_unix_time AS time,
      lb.company_id, i.transaction_id, i.document_id,
      'matched'::text   AS action,
      NULL::text        AS reason,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date',      lb.date_match,
          'order',     lb.order_match,
          'amount',    lb.amount_match,
          'invoice',   lb.invoice_match,
          'currency',  lb.currency_match,
          'customer',  lb.customer_match,
          'merchant',  lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount',   lb.amount_score,
          'invoice',  lb.invoice_score,
          'order',    lb.order_score,
          'date',     lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount',   lb.amount_weight,
          'invoice',  lb.invoice_weight,
          'order',    lb.order_weight,
          'date',     lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence',    lb.confidence_percent,
        'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                               THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                               ELSE NULL END,
        'direction_ok',  lb.amount_direction_ok,
        'date_diff_days',lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* skipped -> reconciliations_activity (if enabled) */
  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_tx',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant', lb.merchant_score,
               'customer', lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency',  lb.currency_weight,
               'merchant',  lb.merchant_weight,
               'customer',  lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_transaction x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL
    SELECT log_run_id, log_run_unix_time, lb.company_id, x.transaction_id, x.document_id,
           'skipped','not_top_for_doc',
           jsonb_build_object(
             'matches', jsonb_build_object(
               'date',      lb.date_match,
               'order',     lb.order_match,
               'amount',    lb.amount_match,
               'invoice',   lb.invoice_match,
               'currency',  lb.currency_match,
               'customer',  lb.customer_match,
               'merchant',  lb.merchant_match
             ),
             'scores', jsonb_build_object(
               'amount',   lb.amount_score,
               'invoice',  lb.invoice_score,
               'order',    lb.order_score,
               'date',     lb.date_score,
               'currency', lb.currency_score,
               'merchant',  lb.merchant_score,
               'customer',  lb.customer_score
             ),
             'weights', jsonb_build_object(
               'amount',   lb.amount_weight,
               'invoice',  lb.invoice_weight,
               'order',    lb.order_weight,
               'date',     lb.date_weight,
               'currency',  lb.currency_weight,
               'merchant',  lb.merchant_weight,
               'customer',  lb.customer_weight
             ),
             'confidence',    lb.confidence_percent,
             'amount_delta',  CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                                    THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                                    ELSE NULL END,
             'direction_ok',  lb.amount_direction_ok,
             'date_diff_days',lb.date_diff_days
           )
    FROM not_top_for_document x JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  /* pair cache: POSITIVE ONLY (matched) */
  cache_upsert_matched AS (
    INSERT INTO public.reconciliations_pair_cache_id (
      company_id, transaction_id, document_id, last_action, last_confidence, last_seen_unix
    )
    SELECT
      lb.company_id, i.transaction_id, i.document_id,
      'matched', lb.confidence_percent, log_run_unix_time
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    ON CONFLICT (transaction_id, document_id) DO UPDATE
    SET last_action     = EXCLUDED.last_action,
        last_confidence = EXCLUDED.last_confidence,
        last_seen_unix  = EXCLUDED.last_seen_unix
    RETURNING 1
  ),

  /* final counts for return and logging */
  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM base_candidates)          AS c_base_candidates,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS c_inserted,
      (SELECT COUNT(*) FROM top_per_transaction)      AS c_top_per_transaction,
      (SELECT COUNT(*) FROM top_per_document)         AS c_top_per_document,
      (SELECT COUNT(*) FROM final_candidates)         AS c_passed_confidence
  )
  SELECT
    c_top_per_transaction, c_top_per_document, c_passed_confidence,
    c_base_candidates, c_inserted
  INTO
    summary_top_per_transaction_count, summary_top_per_document_count, summary_passed_confidence_count,
    v_candidates_count, v_inserted_count
  FROM final_counts;

  -- end log
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'inserted',  v_inserted_count,
      'candidates', v_candidates_count,
      'top_per_tx', summary_top_per_transaction_count,
      'top_per_doc', summary_top_per_document_count,
      'passed_confidence', summary_passed_confidence_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               message_text,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );

EXCEPTION WHEN OTHERS THEN
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  BEGIN
    INSERT INTO public.reconciliations_logs (
      run_id, function_version, action, target, status, message,
      company_id, transaction_id, document_id, reconciliation_id,
      rows_affected, confidence, step_ms, processing_ms, context
    )
    VALUES (
      log_run_id, engine_version, 'run_error', 'function', 'error', SQLERRM,
      p_company_id, NULL, NULL, NULL,
      NULL, NULL, NULL, elapsed_ms,
      jsonb_build_object('hint','see server logs or activity tables')
    );
  EXCEPTION WHEN OTHERS THEN
    NULL;
  END;

  RETURN jsonb_build_object(
    'msg','error',
    'error', SQLERRM,
    'time_ms', elapsed_ms,
    'top_per_tx',0,'top_per_doc',0,'passed_confidence',0
  );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1_9(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  -- Status and versioning
  status_message              text := 'success';
  engine_version              text := '1.9';

  -- Timing
  start_time                  timestamptz := clock_timestamp();
  elapsed_ms                  bigint := 0;

  -- Configuration from DB
  config_amount_tolerance           numeric;  -- percentage or fraction
  config_date_window_days           numeric;
  config_min_confidence             numeric;  -- may be 0–1 or 0–100
  config_allow_document_duplicates  boolean;  -- reserved
  config_log_activity               boolean;

  -- Derived configuration
  derived_amount_tolerance_fraction numeric;  -- always a fraction 0–1
  derived_date_window_seconds       bigint;   -- days -> seconds
  seed_cap_per_match_type           int := 10; -- per-criterion seed cap
  max_transactions_to_scan          int := 20000;

  -- Weight registry
  weights_json                jsonb := '{}'::jsonb;
  weights_total_possible      numeric := 0;     -- sum of all weights

  -- Summary/result counts
  summary_top_per_transaction_count  bigint := 0;
  summary_top_per_document_count     bigint := 0;
  summary_passed_confidence_count    bigint := 0;
  v_inserted_count                   bigint := 0;
  v_candidates_count                 bigint := 0;

  -- Run metadata
  log_run_id                   uuid := gen_random_uuid();
  log_run_unix_time            bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- Error diagnostics
  err_state   text;
  err_msg     text;
  err_detail  text;
  err_hint    text;
  err_context text;

BEGIN

  -- Load configuration
  SELECT amount_tolerance,
         date_window_days,
         min_confidence,
         allow_doc_duplicates,
         log_activity
  INTO   config_amount_tolerance,
         config_date_window_days,
         config_min_confidence,
         config_allow_document_duplicates,
         config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object(
      'msg','reconciliations_config not found',
      'time_ms',elapsed_ms,
      'top_per_tx',0,
      'top_per_doc',0,
      'passed_confidence',0
    );
  END IF;

  -- Derive configuration
  derived_amount_tolerance_fraction := CASE
    WHEN config_amount_tolerance >= 1 THEN config_amount_tolerance / 100.0
    ELSE config_amount_tolerance
  END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN
    config_min_confidence := 0;
  END IF;
  IF config_min_confidence <= 1 THEN
    config_min_confidence := config_min_confidence * 100.0; -- normalize to percent
  END IF;

  -- Resolve weights
  WITH candidate_weights AS (
    SELECT LOWER(TRIM(w.weight_key)) AS k_norm,
           (w.weight_value)::numeric AS v_norm,
           w.company_id
    FROM public.reconciliations_weight_registry w
    WHERE w.active
      AND w.merchant_id IS NULL
      AND (
           (p_company_id IS NULL AND w.company_id IS NULL)
        OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR w.company_id IS NULL))
      )
      AND LOWER(TRIM(w.weight_key)) IN (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked_weights AS (
    SELECT DISTINCT ON (k_norm)
           k_norm, v_norm
    FROM candidate_weights
    ORDER BY k_norm, (company_id IS NULL) -- company-specific first
  ),
  wj AS (
    SELECT jsonb_object_agg(k_norm, v_norm) AS j
    FROM picked_weights
  )
  SELECT COALESCE(j, '{}'::jsonb)
  INTO weights_json
  FROM wj;

  SELECT COALESCE((weights_json->>'amount_total')::numeric,0)
       + COALESCE((weights_json->>'invoice_number')::numeric,0)
       + COALESCE((weights_json->>'order_number')::numeric,0)
       + COALESCE((weights_json->>'date')::numeric,0)
       + COALESCE((weights_json->>'currency')::numeric,0)
       + COALESCE((weights_json->>'merchant_id')::numeric,0)
       + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_total_possible;

  IF weights_total_possible = 0 THEN
    weights_json := jsonb_build_object(
      'amount_total', 1,
      'invoice_number', 1,
      'order_number', 1,
      'date', 1,
      'currency', 1,
      'merchant_id', 1,
      'customer_name', 1
    );
    weights_total_possible := 7;
  END IF;

  -- Log run_start
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', config_log_activity,
        'allow_doc_dups', config_allow_document_duplicates,
        'amount_tol_pct', derived_amount_tolerance_fraction,
        'min_confidence', config_min_confidence,
        'date_window_days', config_date_window_days,
        'max_tx_seed', max_transactions_to_scan,
        'seed_cap_per_criterion', seed_cap_per_match_type
      ),
      'weights_json', weights_json,
      'weights_max_possible', weights_total_possible
    )
  );

  -- Main pipeline
  WITH
  document_scope AS MATERIALIZED (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,
      d.document_date_unix,
      d.merchant_id,
      d.customer_name
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS MATERIALIZED (
    SELECT
      t.transaction_id,
      t.company_id,      -- text in source table
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,
      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND t.name_consumer IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT max_transactions_to_scan
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
          OR (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR (d.currency = t.currency AND d.customer_name = t.name_consumer)
          OR (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(d.document_date_unix - t.completed_date) ASC,
        d.document_id
      LIMIT (seed_cap_per_match_type * 6)
    ) d ON TRUE
  ),

  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id
    FROM candidate_pairs
  ),

  attributes AS (
    SELECT
      p.transaction_id,
      p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount,
      t.debited AS transaction_debited,
      t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency,
      d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number,
      d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id,
      d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix,
      d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id,
      d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name,
      d.customer_name AS document_customer_name,
      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,

      COALESCE((a.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
                AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,
      COALESCE((a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
                AND a.transaction_order_id = a.document_order_id), FALSE)             AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency,  FALSE)          AS currency_match,
      COALESCE(a.transaction_merchant_id  = a.document_merchant_id,  FALSE)    AS merchant_match,
      COALESCE(a.transaction_customer_name  = a.document_customer_name,  FALSE)AS customer_match,
      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,
      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,

      e.invoice_number_match,
      e.order_number_match,
      e.date_match,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,

      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE
        ELSE FALSE
      END AS amount_tolerance_match,

      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS amount_weight,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS order_weight,
      COALESCE((weights_json->>'date')::numeric, 0)           AS date_weight,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS currency_weight,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS merchant_weight,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS customer_weight
  ),

  scored_components AS (
    SELECT
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN GREATEST(
            0,
            LEAST(
              w.amount_weight,
              w.amount_weight * (1 - (
                ABS(m.transaction_effective_amount - m.document_total)
                / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)
              ))
            )
          )
          ELSE 0
        END,
      0)::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,

      w.amount_weight   AS amount_weight,
      w.invoice_weight  AS invoice_weight,
      w.order_weight    AS order_weight,
      w.date_weight     AS date_weight,
      w.currency_weight AS currency_weight,
      w.merchant_weight AS merchant_weight,
      w.customer_weight AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.invoice_weight)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.order_weight)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.date_weight)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.currency_weight)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.merchant_weight)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.customer_weight)::numeric AS customer_score,

      m.transaction_effective_amount,
      m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_confidence AS (
    SELECT
      s.transaction_id,
      s.document_id,
      s.document_company_id,
      s.transaction_date_unix,
      s.document_date_unix,
      s.amount_direction_ok,

      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,

      CASE WHEN weights_total_possible > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                / weights_total_possible) * 100.0, 4)
      ELSE 0
      END AS confidence_percent,

      s.amount_match,
      s.invoice_match,
      s.order_match,
      s.date_match,
      s.currency_match,
      s.merchant_match,
      s.customer_match,

      s.date_weight,
      s.invoice_weight,
      s.order_weight,
      s.merchant_weight,
      s.amount_weight,
      s.currency_weight,
      s.customer_weight,

      s.date_score,
      s.invoice_score,
      s.order_score,
      s.merchant_score,
      s.amount_score,
      s.currency_score,
      s.customer_score,

      s.transaction_effective_amount,
      s.document_total
    FROM scored_components s
  ),

  -- Thresholds and ranking
  low_signal AS (
    SELECT * FROM scored_with_confidence WHERE signals_matched_count < 1
  ),
  below_min_confidence AS (
    SELECT * FROM scored_with_confidence WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE  signals_matched_count >= 1
       AND confidence_percent >= config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  not_top_for_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction > 1
  ),
  top_per_transaction AS (
    SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1
  ),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY t.confidence_percent DESC, t.total_numerator DESC, t.signals_matched_count DESC, t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  not_top_for_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document > 1
  ),
  top_per_document AS (
    SELECT * FROM ranked_per_document WHERE rownum_per_document = 1
  ),

  final_candidates AS (
    SELECT *
    FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),

  -- Membership gate against any prior reconciliation
  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (
             SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id
           )
       AND NOT EXISTS (
             SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id
           )
  ),

  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary
    )
    WITH ts_base AS (
      SELECT
        f.transaction_id,
        f.document_id,
        f.document_company_id,
        f.confidence_percent,
        COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id,
        d.document_id,
        d.document_company_id,
        d.confidence_percent,
        d.invoice_match,
        d.order_match,
        d.amount_match,
        d.date_match,
        d.currency_match,
        d.merchant_match,
        d.customer_match,
        d.transaction_effective_amount,
        d.document_total,
        d.transaction_date_unix,
        d.document_date_unix,
        t.base_unix,
        a.transaction_invoice_number,
        a.document_invoice_number,
        a.transaction_order_id,
        a.document_order_id,
        a.transaction_currency,
        a.document_currency,
        a.transaction_merchant_id,
        a.document_merchant_id,
        a.transaction_customer_name,
        a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d ON d.transaction_id = t.transaction_id AND d.document_id = t.document_id
      JOIN attributes a       ON a.transaction_id = d.transaction_id AND a.document_id = d.document_id
    )
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,
      COALESCE(e.confidence_percent, 0)::numeric,
      log_run_unix_time,
      engine_version,
      EXTRACT(YEAR    FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
          SELECT json_agg(s) FROM unnest(ARRAY[
            CASE WHEN e.invoice_match  THEN 'invoice'  END,
            CASE WHEN e.amount_match   THEN 'amount'   END,
            CASE WHEN e.date_match     THEN 'date'     END,
            CASE WHEN e.currency_match THEN 'currency' END,
            CASE WHEN e.merchant_match THEN 'merchant' END,
            CASE WHEN e.customer_match THEN 'customer' END,
            CASE WHEN e.order_match    THEN 'order'    END
          ]) s WHERE s IS NOT NULL
        ),
        'tx_amount',    e.transaction_effective_amount,
        'doc_total',    e.document_total,
        'tx_invoice',   e.transaction_invoice_number,
        'doc_invoice',  e.document_invoice_number,
        'tx_order',     e.transaction_order_id,
        'doc_order',    e.document_order_id,
        'tx_currency',  e.transaction_currency,
        'doc_currency', e.document_currency,
        'tx_merchant',  e.transaction_merchant_id,
        'doc_merchant', e.document_merchant_id,
        'tx_customer',  e.transaction_customer_name,
        'doc_customer', e.document_customer_name,
        'tx_unix',      e.transaction_date_unix,
        'doc_unix',     e.document_date_unix
      )::text
    FROM enrich e
    RETURNING reconciliation_id, transaction_id, document_id, confidence
  ),

  inserted_match_scores AS (
    INSERT INTO public.reconciliations_match_scores (
      reconciliation_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
      r.reconciliation_id,
      r.confidence,
      d.total_numerator,
      weights_total_possible,
      d.date_match,
      d.invoice_match,
      d.order_match,
      d.merchant_match,
      (d.amount_match)::bool,
      d.currency_match,
      d.customer_match,
      d.date_weight,
      d.invoice_weight,
      d.order_weight,
      d.merchant_weight,
      d.amount_weight,
      d.currency_weight,
      d.customer_weight,
      d.date_score,
      d.invoice_score,
      d.order_score,
      d.merchant_score,
      d.amount_score,
      d.currency_score,
      d.customer_score
    FROM inserted_reconciliations r
    JOIN final_candidates d ON d.transaction_id = r.transaction_id AND d.document_id = r.document_id
    RETURNING 1
  ),

  -- Optional activity logging
  logging_base AS (
    SELECT
      s.transaction_id,
      s.document_id,
      s.document_company_id AS company_id,
      s.confidence_percent,
      s.date_match,
      s.order_match,
      s.amount_match,
      s.invoice_match,
      s.currency_match,
      s.customer_match,
      s.merchant_match,
      s.amount_score,
      s.invoice_score,
      s.order_score,
      s.date_score,
      s.currency_score,
      s.merchant_score,
      s.customer_score,
      s.amount_weight,
      s.invoice_weight,
      s.order_weight,
      s.date_weight,
      s.currency_weight,
      s.merchant_weight,
      s.customer_weight,
      s.transaction_effective_amount,
      s.document_total,
      s.amount_direction_ok,
      CASE
        WHEN s.transaction_date_unix IS NOT NULL AND s.document_date_unix IS NOT NULL THEN
          (ABS(s.transaction_date_unix - s.document_date_unix) / 86400.0)::numeric
        ELSE NULL::numeric
      END AS date_diff_days
    FROM scored_with_confidence s
  ),

  logging_matched_rows AS (
    INSERT INTO public.reconciliation_activity (
      reconciliation_id, run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      i.reconciliation_id,
      log_run_id,
      log_run_unix_time,
      lb.company_id,
      i.transaction_id,
      i.document_id,
      'matched'::text,
      NULL::text,
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match,
          'order', lb.order_match,
          'amount', lb.amount_match,
          'invoice', lb.invoice_match,
          'currency', lb.currency_match,
          'customer', lb.customer_match,
          'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score,
          'invoice', lb.invoice_score,
          'order', lb.order_score,
          'date', lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight,
          'invoice', lb.invoice_weight,
          'order', lb.order_weight,
          'date', lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                              THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                              ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM inserted_reconciliations i
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  logging_skipped_union AS (
    INSERT INTO public.reconciliations_activity (
      run_id, time, company_id, transaction_id, document_id, action, reason, details
    )
    SELECT
      log_run_id,
      log_run_unix_time,
      lb.company_id,
      x.transaction_id,
      x.document_id,
      'skipped',
      'not_top_for_tx',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match,
          'order', lb.order_match,
          'amount', lb.amount_match,
          'invoice', lb.invoice_match,
          'currency', lb.currency_match,
          'customer', lb.customer_match,
          'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score,
          'invoice', lb.invoice_score,
          'order', lb.order_score,
          'date', lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight,
          'invoice', lb.invoice_weight,
          'order', lb.order_weight,
          'date', lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_transaction x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL

    SELECT
      log_run_id,
      log_run_unix_time,
      lb.company_id,
      x.transaction_id,
      x.document_id,
      'skipped',
      'not_top_for_doc',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match,
          'order', lb.order_match,
          'amount', lb.amount_match,
          'invoice', lb.invoice_match,
          'currency', lb.currency_match,
          'customer', lb.customer_match,
          'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score,
          'invoice', lb.invoice_score,
          'order', lb.order_score,
          'date', lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight,
          'invoice', lb.invoice_weight,
          'order', lb.order_weight,
          'date', lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM not_top_for_document x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL

    SELECT
      log_run_id,
      log_run_unix_time,
      lb.company_id,
      x.transaction_id,
      x.document_id,
      'skipped',
      'below_min_confidence',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match,
          'order', lb.order_match,
          'amount', lb.amount_match,
          'invoice', lb.invoice_match,
          'currency', lb.currency_match,
          'customer', lb.customer_match,
          'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score,
          'invoice', lb.invoice_score,
          'order', lb.order_score,
          'date', lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight,
          'invoice', lb.invoice_weight,
          'order', lb.order_weight,
          'date', lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM below_min_confidence x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity

    UNION ALL

    SELECT
      log_run_id,
      log_run_unix_time,
      lb.company_id,
      x.transaction_id,
      x.document_id,
      'skipped',
      'no_signals',
      jsonb_build_object(
        'matches', jsonb_build_object(
          'date', lb.date_match,
          'order', lb.order_match,
          'amount', lb.amount_match,
          'invoice', lb.invoice_match,
          'currency', lb.currency_match,
          'customer', lb.customer_match,
          'merchant', lb.merchant_match
        ),
        'scores', jsonb_build_object(
          'amount', lb.amount_score,
          'invoice', lb.invoice_score,
          'order', lb.order_score,
          'date', lb.date_score,
          'currency', lb.currency_score,
          'merchant', lb.merchant_score,
          'customer', lb.customer_score
        ),
        'weights', jsonb_build_object(
          'amount', lb.amount_weight,
          'invoice', lb.invoice_weight,
          'order', lb.order_weight,
          'date', lb.date_weight,
          'currency', lb.currency_weight,
          'merchant', lb.merchant_weight,
          'customer', lb.customer_weight
        ),
        'confidence', lb.confidence_percent,
        'amount_delta', CASE WHEN lb.transaction_effective_amount IS NOT NULL AND lb.document_total IS NOT NULL
                             THEN ROUND(ABS(lb.transaction_effective_amount - lb.document_total)::numeric, 4)
                             ELSE NULL END,
        'direction_ok', lb.amount_direction_ok,
        'date_diff_days', lb.date_diff_days
      )
    FROM low_signal x
    JOIN logging_base lb USING (transaction_id, document_id)
    WHERE config_log_activity
    RETURNING 1
  ),

  final_counts AS (
    SELECT
      (SELECT COUNT(*) FROM top_per_transaction)      AS top_per_transaction_count,
      (SELECT COUNT(*) FROM top_per_document)         AS top_per_document_count,
      (SELECT COUNT(*) FROM final_candidates)         AS passed_confidence_count,
      (SELECT COUNT(*) FROM inserted_reconciliations) AS inserted_count,
      (SELECT COUNT(*) FROM base_candidates)          AS candidates_count
  )
  SELECT
    top_per_transaction_count,
    top_per_document_count,
    passed_confidence_count,
    inserted_count,
    candidates_count
  INTO
    summary_top_per_transaction_count,
    summary_top_per_document_count,
    summary_passed_confidence_count,
    v_inserted_count,
    v_candidates_count
  FROM final_counts;

  -- Timing end and run_end log
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_end', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    v_inserted_count, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'inserted', v_inserted_count,
      'candidates', v_candidates_count,
      'top_per_tx', summary_top_per_transaction_count,
      'top_per_doc', summary_top_per_document_count,
      'passed_confidence', summary_passed_confidence_count
    )
  );

  RETURN jsonb_build_object(
    'msg',               status_message,
    'time_ms',           elapsed_ms,
    'top_per_tx',        summary_top_per_transaction_count,
    'top_per_doc',       summary_top_per_document_count,
    'passed_confidence', summary_passed_confidence_count
  );

EXCEPTION WHEN OTHERS THEN
  -- Error handling
  elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;

  GET STACKED DIAGNOSTICS
    err_state   = RETURNED_SQLSTATE,
    err_msg     = MESSAGE_TEXT,
    err_detail  = PG_EXCEPTION_DETAIL,
    err_hint    = PG_EXCEPTION_HINT,
    err_context = PG_EXCEPTION_CONTEXT;

  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_error', 'function', 'error', err_msg,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, elapsed_ms,
    jsonb_build_object(
      'sqlstate', err_state,
      'detail',   err_detail,
      'hint',     err_hint,
      'context',  err_context
    )
  );

  RETURN jsonb_build_object(
    'msg','error',
    'time_ms',elapsed_ms,
    'top_per_tx',0,
    'top_per_doc',0,
    'passed_confidence',0,
    'error',err_msg,
    'sqlstate',err_state
  );
END;
$$;

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

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

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

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

create or replace function add_merchant_iban(body jsonb) returns merchant_ibans
    security definer
    SET search_path = public
    language plpgsql
as
$$
DECLARE v_slug_in text := nullif(
        trim(
            both
            from body->>'slug'
        ),
        ''
    );
v_company_name text := nullif(
    trim(
        both
        from body->>'company_name'
    ),
    ''
);
v_account_holder text := nullif(
    trim(
        both
        from body->>'account_holder'
    ),
    ''
);
v_name text := coalesce(
    v_company_name,
    v_account_holder,
    nullif(
        trim(
            both
            from body->>'name'
        ),
        ''
    )
);
v_icon text := nullif(body->>'icon', '');
v_email text := nullif(body->>'email', '');
v_website text := nullif(body->>'website', '');
v_description text := nullif(body->>'description', '');
v_billing_postal text := nullif(body->>'billing_postal_code', '');
v_billing_line_1 text := nullif(body->>'billing_address_line_1', '');
v_billing_line_2 text := nullif(body->>'billing_address_line_2', '');
v_language text := nullif(body->>'language', '');
v_industry text := nullif(body->>'industry', '');
v_phone text := nullif(body->>'phone_number', '');
v_public boolean := coalesce((body->>'public')::boolean, true);
v_vat_id text := nullif(body->>'vat_id', '');
v_billing_city text := nullif(body->>'billing_city', '');
v_billing_state text := nullif(body->>'billing_state', '');
v_billing_province text := nullif(body->>'billing_province', '');
v_company_number text := nullif(body->>'company_number', '');
v_company_num_type text := nullif(body->>'company_number_type', '');
v_country_code text := nullif(body->>'country_code', '');
v_iban_raw text := body->>'iban';
v_iban text := upper(
    regexp_replace(coalesce(body->>'iban', ''), '\s', '', 'g')
);
v_bank text := nullif(body->>'bank', '');
v_bic text := nullif(body->>'bic', '');
v_iban_desc text := nullif(body->>'iban_description', '');
v_iban_country_code text := nullif(body->>'iban_country_code', '');
v_slug text;
v_base_slug text;
v_try_slug text;
v_suffix int := 1;
v_merchant_id uuid;
v_row public.merchant_ibans;
v_has_unaccent boolean := false;
BEGIN IF v_iban_raw IS NULL
OR v_iban = '' THEN RAISE EXCEPTION 'iban is required' USING ERRCODE = '22023';
END IF;
IF v_slug_in IS NULL
AND (
    v_name IS NULL
    OR v_name = ''
) THEN RAISE EXCEPTION 'company_name or account_holder (or name) is required to auto-generate slug' USING ERRCODE = '22023';
END IF;
SELECT EXISTS (
        SELECT 1
        FROM pg_extension
        WHERE extname = 'unaccent'
    ) INTO v_has_unaccent;
v_base_slug := coalesce(v_slug_in, lower(coalesce(v_name, 'merchant')));
IF v_has_unaccent THEN v_base_slug := unaccent(v_base_slug);
END IF;
v_base_slug := regexp_replace(v_base_slug, '[^a-z0-9]+', '-', 'g');
v_base_slug := regexp_replace(v_base_slug, '(^-+)|(-+$)', '', 'g');
IF v_base_slug IS NULL
OR v_base_slug = '' THEN v_base_slug := 'merchant';
END IF;
v_try_slug := v_base_slug;
v_merchant_id := NULL;
SELECT m.merchant_id INTO v_merchant_id
FROM public.global_merchants_registry m
WHERE m.slug = v_try_slug;
v_slug := v_try_slug;
IF v_merchant_id IS NULL THEN
INSERT INTO public.global_merchants_registry (
        name,
        icon,
        email,
        website,
        description,
        billing_postal_code,
        billing_address_line_1,
        billing_address_line_2,
        language,
        industry,
        phone_number,
        slug,
        enabled,
        country_code,
        public,
        vat_id,
        billing_city,
        billing_state,
        billing_province,
        company_number,
        company_number_type
    )
VALUES (
        v_name,
        v_icon,
        v_email,
        v_website,
        v_description,
        v_billing_postal,
        v_billing_line_1,
        v_billing_line_2,
        v_language,
        v_industry,
        v_phone,
        v_slug,
        true,
        v_country_code,
        v_public,
        v_vat_id,
        v_billing_city,
        v_billing_state,
        v_billing_province,
        v_company_number,
        v_company_num_type
    )
RETURNING merchant_id INTO v_merchant_id;
END IF;
INSERT INTO public.merchant_ibans (
        merchant_id,
        iban,
        country_code,
        bank,
        description,
        bic,
        slug
    )
VALUES (
        v_merchant_id,
        v_iban,
        coalesce(v_iban_country_code, v_country_code),
        v_bank,
        coalesce(v_iban_desc, v_description),
        v_bic,
        v_slug
    )
RETURNING * INTO v_row;
RETURN v_row;
EXCEPTION
WHEN unique_violation THEN IF SQLERRM LIKE '%merchant_ibans_iban_key%' THEN RAISE EXCEPTION 'IBAN % already exists',
v_iban USING ERRCODE = '23505';
ELSE RAISE;
END IF;
WHEN others THEN RAISE;
END;
$$;

alter function add_merchant_iban(unknown) owner to postgres;

grant execute on function add_merchant_iban(unknown) to anon;

grant execute on function add_merchant_iban(unknown) to authenticated;

grant execute on function add_merchant_iban(unknown) to service_role;

create or replace function add_merchant_iban(slug text DEFAULT NULL::text, company_name text DEFAULT NULL::text, iban text DEFAULT NULL::text, bank text DEFAULT NULL::text, bic text DEFAULT NULL::text, iban_description text DEFAULT NULL::text, country_code text DEFAULT NULL::text, email text DEFAULT NULL::text) returns merchant_ibans
    language plpgsql
as
$$
DECLARE
  -- local variables
  v_slug text := slug;
  v_name text := company_name;
  v_iban text := upper(regexp_replace(coalesce(iban, ''), '\s', '', 'g'));
  v_row public.merchant_ibans;
  v_merchant_id uuid;
BEGIN
  IF v_iban IS NULL OR v_iban = '' THEN
    RAISE EXCEPTION 'iban is required' USING ERRCODE = '22023';
  END IF;

  -- create/find merchant
  SELECT m.merchant_id INTO v_merchant_id
  FROM public.global_merchants_registry m
  WHERE m.slug = v_slug;

  IF v_merchant_id IS NULL THEN
    INSERT INTO public.global_merchants_registry (name, slug, email, country_code)
    VALUES (v_name, v_slug, email, country_code)
    RETURNING merchant_id INTO v_merchant_id;
  END IF;

  -- insert iban
  INSERT INTO public.merchant_ibans (merchant_id, slug, iban, country_code, bank, description, bic)
  VALUES (v_merchant_id, v_slug, v_iban, country_code, bank, iban_description, bic)
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function find_merchant_by_slug(slug text) returns jsonb
    security definer
    SET search_path = public
    language plpgsql
as
$$
DECLARE
  v_result jsonb;
  v_merchant public.global_merchants_registry;
BEGIN
  -- Fetch merchant by slug
  SELECT m.* INTO v_merchant
  FROM public.global_merchants_registry m
  WHERE m.slug = slug;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'Merchant with slug % not found', slug USING ERRCODE = 'P0002';
  END IF;

  -- Build JSON result: merchant + ibans
  SELECT jsonb_build_object(
    'merchant', to_jsonb(v_merchant),
    'ibans', (
      SELECT jsonb_agg(to_jsonb(i))
      FROM public.merchant_ibans i
      WHERE i.slug = slug
      ORDER BY i.created_at
    )
  )
  INTO v_result;

  RETURN v_result;
END;
$$;

alter function find_merchant_by_slug(unknown) owner to postgres;

grant execute on function find_merchant_by_slug(unknown) to anon;

grant execute on function find_merchant_by_slug(unknown) to authenticated;

grant execute on function find_merchant_by_slug(unknown) to service_role;

create or replace function athena_reconcile_general_v2_0(p_company_id uuid, p_reconciliation_config_id uuid DEFAULT '3dae610e-2326-46b8-ba18-85f54961e4ca'::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
  status_message text := 'success';
  engine_version text := '2.0';

  start_time timestamptz := clock_timestamp();
  elapsed_ms bigint := 0;

  -- config from DB
  config_amount_tolerance           numeric;
  config_date_window_days           numeric;
  config_min_confidence             numeric;
  config_allow_document_duplicates  boolean;
  config_log_activity               boolean;

  -- derived
  derived_amount_tolerance_fraction numeric;
  derived_date_window_seconds       bigint;
  seed_cap_per_match_type           int := 10;
  max_transactions_to_scan          int := 20000;

  -- weights
  weights_json           jsonb := '{}'::jsonb;
  weights_total_possible numeric := 0;

  -- counters
  summary_top_per_transaction_count  bigint := 0;
  summary_top_per_document_count     bigint := 0;
  summary_passed_confidence_count    bigint := 0;
  v_inserted_count                   bigint := 0;
  v_candidates_count                 bigint := 0;

  -- run meta
  log_run_id        uuid := gen_random_uuid();
  log_run_unix_time bigint := EXTRACT(EPOCH FROM now())::bigint;

  -- optional rule: direction-aware matching for names/IBAN
  directional_matching boolean := TRUE;  -- set FALSE to allow name/IBAN matching regardless of in/out

  -- errors
  err_state   text;
  err_msg     text;
  err_detail  text;
  err_hint    text;
  err_context text;

BEGIN
  -- config load
  SELECT amount_tolerance, date_window_days, min_confidence, allow_doc_duplicates, log_activity
  INTO   config_amount_tolerance, config_date_window_days, config_min_confidence,
         config_allow_document_duplicates, config_log_activity
  FROM   public.reconciliations_config
  WHERE  reconciliation_config_id = p_reconciliation_config_id;

  IF NOT FOUND THEN
    elapsed_ms := FLOOR(EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) * 1000)::bigint;
    RETURN jsonb_build_object('msg','reconciliations_config not found','time_ms',elapsed_ms,
                              'top_per_tx',0,'top_per_doc',0,'passed_confidence',0);
  END IF;

  -- derive
  derived_amount_tolerance_fraction := CASE
    WHEN config_amount_tolerance >= 1 THEN config_amount_tolerance / 100.0
    ELSE config_amount_tolerance
  END;

  derived_date_window_seconds := CEIL(COALESCE(config_date_window_days, 1) * 86400)::bigint;

  IF config_min_confidence IS NULL THEN config_min_confidence := 0; END IF;
  IF config_min_confidence <= 1 THEN config_min_confidence := config_min_confidence * 100.0; END IF;

  -- weights
  WITH candidate_weights AS (
    SELECT LOWER(TRIM(w.weight_key)) AS k_norm,
           (w.weight_value)::numeric AS v_norm,
           w.company_id
    FROM public.reconciliations_weight_registry w
    WHERE w.active
      AND w.merchant_id IS NULL
      AND (
           (p_company_id IS NULL AND w.company_id IS NULL)
        OR (p_company_id IS NOT NULL AND (w.company_id = p_company_id OR w.company_id IS NULL))
      )
      AND LOWER(TRIM(w.weight_key)) IN (
        'amount_total','invoice_number','order_number','date','currency','merchant_id','customer_name'
      )
  ),
  picked AS (
    SELECT DISTINCT ON (k_norm) k_norm, v_norm
    FROM candidate_weights
    ORDER BY k_norm, (company_id IS NULL)
  )
  SELECT COALESCE(jsonb_object_agg(k_norm, v_norm), '{}'::jsonb) INTO weights_json FROM picked;

  SELECT COALESCE((weights_json->>'amount_total')::numeric,0)
       + COALESCE((weights_json->>'invoice_number')::numeric,0)
       + COALESCE((weights_json->>'order_number')::numeric,0)
       + COALESCE((weights_json->>'date')::numeric,0)
       + COALESCE((weights_json->>'currency')::numeric,0)
       + COALESCE((weights_json->>'merchant_id')::numeric,0)
       + COALESCE((weights_json->>'customer_name')::numeric,0)
  INTO weights_total_possible;

  IF weights_total_possible = 0 THEN
    weights_json := jsonb_build_object(
      'amount_total',1,'invoice_number',1,'order_number',1,'date',1,'currency',1,'merchant_id',1,'customer_name',1
    );
    weights_total_possible := 7;
  END IF;

  -- run_start log
  INSERT INTO public.reconciliations_logs (
    run_id, function_version, action, target, status, message,
    company_id, transaction_id, document_id, reconciliation_id,
    rows_affected, confidence, step_ms, processing_ms, context
  )
  VALUES (
    log_run_id, engine_version, 'run_start', 'function', 'ok', NULL,
    p_company_id, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,
    jsonb_build_object(
      'args', jsonb_build_object(
        'log_activity', config_log_activity,
        'allow_doc_dups', config_allow_document_duplicates,
        'amount_tol_pct', derived_amount_tolerance_fraction,
        'min_confidence', config_min_confidence,
        'date_window_days', config_date_window_days,
        'max_tx_seed', max_transactions_to_scan,
        'seed_cap_per_criterion', seed_cap_per_match_type,
        'directional_matching', directional_matching
      ),
      'weights_json', weights_json,
      'weights_max_possible', weights_total_possible
    )
  );

  -- pipeline
  WITH
  document_scope AS MATERIALIZED (
    SELECT
      d.document_id,
      d.company_id,
      d.total,
      d.currency,
      NULLIF(d.invoice_number, '') AS invoice_number,
      NULLIF(d.order_id, '')       AS order_id,

      -- derive document_date_unix without external function
      CASE
        WHEN d.document_date_unix IS NOT NULL THEN d.document_date_unix
        WHEN d.document_date ~ '^\d{4}-[A-Za-z]+-\d{2}$' THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'YYYY-Month-DD'))::bigint
        WHEN d.document_date ~ '^\d{2}-[A-Za-z]+-\d{4}$' THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'DD-Month-YYYY'))::bigint
        WHEN d.document_date ~ '^\d{4}-\d{2}-\d{2}$'     THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'YYYY-MM-DD'))::bigint
        WHEN d.document_date ~ '^\d{2}-\d{2}-\d{4}$'     THEN EXTRACT(EPOCH FROM to_timestamp(d.document_date,'DD-MM-YYYY'))::bigint
        ELSE NULL
      END AS document_date_unix,

      d.merchant_id,
      d.customer_name,

      -- normalized names + IBAN
      NULLIF(public.norm_core_ns(COALESCE(d.merchant_name, d.merchant_info::jsonb->>'name')), '') AS doc_merchant_ns,
      NULLIF(public.norm_text_ns(
        concat_ws(' ',
          d.customer_name,
          d.customer_info::jsonb->>'name',
          d.customer_info::jsonb->>'contact_person',
          d.customer_info::jsonb->>'company_name',
          d.customer_info::jsonb->>'email'
        )
      ), '') AS doc_customer_ns,
      NULLIF(public.norm_iban(d.merchant_info::jsonb->>'iban'), '') AS doc_iban_ns
    FROM public.documents_v2 d
    WHERE (p_company_id IS NULL OR d.company_id = p_company_id)
      AND d.reconciled IS NOT TRUE
  ),

  transactions_scope AS MATERIALIZED (
    SELECT
      t.transaction_id,
      t.company_id,
      t.amount,
      t.debited,
      t.credited,
      t.currency,
      NULLIF(t.invoice_nr, '') AS invoice_nr,
      NULLIF(t.order_id,  '')  AS order_id,
      t.completed_date,
      t.merchant_id,
      t.name_consumer,

      -- direction flags
      (NULLIF(t.credited,0) IS NOT NULL OR NULLIF(t.money_in,0) IS NOT NULL)  AS tx_is_in,
      (NULLIF(t.debited,0)  IS NOT NULL OR NULLIF(t.money_out,0) IS NOT NULL) AS tx_is_out,

      -- normalized names + IBAN
      NULLIF(public.norm_core_ns(
        COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
      ), '') AS tx_merchant_ns,

      -- build customer from multiple fields
      NULLIF(public.norm_text_ns(
        COALESCE(
          t.name_consumer,
          t.account_holder_name,               -- own account holder name
          t.account_holder_name_counterparty   -- counterparty name if mapped here
        )
      ), '') AS tx_customer_ns,

      NULLIF(public.norm_iban(COALESCE(t.iban_counterparty, t.iban_beneficiary, NULLIF(t.iban,''))), '') AS tx_iban_ns,

      CASE
        WHEN t.debited  IS NOT NULL AND t.debited  > 0 THEN t.debited
        WHEN t.credited IS NOT NULL AND t.credited > 0 THEN t.credited
        WHEN t.amount   IS NOT NULL                  THEN ABS(t.amount)
        ELSE NULL
      END AS effective_amount
    FROM public.transactions t
    WHERE (p_company_id IS NULL OR t.company_id = p_company_id::text)
      AND t.reconciled IS NOT TRUE
      AND (
           t.invoice_nr IS NOT NULL
        OR t.order_id   IS NOT NULL
        OR (t.currency IS NOT NULL AND t.merchant_id   IS NOT NULL)
        OR (t.currency IS NOT NULL AND t.name_consumer IS NOT NULL)
        OR (t.currency IS NOT NULL AND (t.amount IS NOT NULL OR t.debited IS NOT NULL OR t.credited IS NOT NULL))
      )
    ORDER BY t.completed_date DESC, t.transaction_id
    LIMIT max_transactions_to_scan
  ),

  candidate_pairs AS (
    SELECT t.transaction_id, d.document_id
    FROM transactions_scope t
    JOIN LATERAL (
      SELECT d.document_id
      FROM document_scope d
      WHERE d.company_id = t.company_id::uuid
        AND (
          -- strong keys
          (d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr)
          OR (d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id)
          OR (d.currency = t.currency AND d.merchant_id   = t.merchant_id)
          OR (d.currency = t.currency AND d.customer_name = t.name_consumer)

          -- name seeding with date window; direction optional
          OR (((NOT directional_matching) OR t.tx_is_out) AND d.currency = t.currency
              AND d.document_date_unix IS NOT NULL
              AND ABS(d.document_date_unix - t.completed_date) <= derived_date_window_seconds
              AND d.doc_merchant_ns IS NOT NULL AND t.tx_merchant_ns IS NOT NULL
              AND (
                   d.doc_merchant_ns = t.tx_merchant_ns
                OR position(d.doc_merchant_ns in t.tx_merchant_ns) > 0
                OR position(t.tx_merchant_ns in d.doc_merchant_ns) > 0
              ))
          OR (((NOT directional_matching) OR t.tx_is_in) AND d.currency = t.currency
              AND d.document_date_unix IS NOT NULL
              AND ABS(d.document_date_unix - t.completed_date) <= derived_date_window_seconds
              AND d.doc_customer_ns IS NOT NULL AND t.tx_customer_ns IS NOT NULL
              AND (
                   d.doc_customer_ns = t.tx_customer_ns
                OR position(d.doc_customer_ns in t.tx_customer_ns) > 0
                OR position(t.tx_customer_ns in d.doc_customer_ns) > 0
              ))

          -- IBAN seeding for outgoing normally; direction optional
          OR (((NOT directional_matching) OR t.tx_is_out) AND d.currency = t.currency
              AND d.document_date_unix IS NOT NULL
              AND ABS(d.document_date_unix - t.completed_date) <= derived_date_window_seconds
              AND t.tx_iban_ns IS NOT NULL AND d.doc_iban_ns IS NOT NULL
              AND t.tx_iban_ns = d.doc_iban_ns)

          -- amount path
          OR (d.currency = t.currency AND (
               d.total = t.effective_amount
            OR ( t.effective_amount IS NOT NULL
                 AND d.total IS NOT NULL
                 AND ABS(d.total - t.effective_amount) <= d.total * derived_amount_tolerance_fraction
               )
          ))
        )
      ORDER BY
        CASE
          WHEN d.invoice_number IS NOT NULL AND t.invoice_nr IS NOT NULL AND d.invoice_number = t.invoice_nr THEN 0
          WHEN d.order_id       IS NOT NULL AND t.order_id   IS NOT NULL AND d.order_id       = t.order_id   THEN 1
          WHEN d.currency = t.currency AND d.total = t.effective_amount THEN 2
          ELSE 3
        END,
        ABS(COALESCE(d.document_date_unix,0) - t.completed_date) ASC,
        d.document_id
      LIMIT (seed_cap_per_match_type * 6)
    ) d ON TRUE
  ),

  distinct_pairs AS (
    SELECT DISTINCT transaction_id, document_id
    FROM candidate_pairs
  ),

  attributes AS (
    SELECT
      p.transaction_id,
      p.document_id,
      t.company_id::uuid AS transaction_company_id,
      d.company_id       AS document_company_id,

      t.amount AS transaction_amount,
      t.debited AS transaction_debited,
      t.credited AS transaction_credited,
      d.total  AS document_total,
      t.currency AS transaction_currency,
      d.currency AS document_currency,
      t.invoice_nr AS transaction_invoice_number,
      d.invoice_number AS document_invoice_number,
      t.order_id AS transaction_order_id,
      d.order_id AS document_order_id,
      t.completed_date AS transaction_date_unix,
      d.document_date_unix AS document_date_unix,
      t.merchant_id AS transaction_merchant_id,
      d.merchant_id AS document_merchant_id,
      t.name_consumer AS transaction_customer_name,
      d.customer_name AS document_customer_name,

      t.tx_is_in, t.tx_is_out,
      t.tx_merchant_ns, d.doc_merchant_ns,
      t.tx_customer_ns, d.doc_customer_ns,
      t.tx_iban_ns,     d.doc_iban_ns,

      t.effective_amount AS transaction_effective_amount
    FROM distinct_pairs p
    JOIN transactions_scope t ON t.transaction_id = p.transaction_id
    JOIN document_scope    d ON d.document_id    = p.document_id
    WHERE t.company_id::uuid = d.company_id
  ),

  evaluated AS (
    SELECT
      a.transaction_id,
      a.document_id,
      a.transaction_effective_amount,

      COALESCE((a.transaction_invoice_number IS NOT NULL AND a.document_invoice_number IS NOT NULL
                AND a.transaction_invoice_number = a.document_invoice_number), FALSE) AS invoice_number_match,
      COALESCE((a.transaction_order_id IS NOT NULL AND a.document_order_id IS NOT NULL
                AND a.transaction_order_id = a.document_order_id), FALSE) AS order_number_match,
      COALESCE(a.transaction_currency  = a.document_currency, FALSE) AS currency_match,

      -- merchant: id always; name/IBAN gated by direction if enabled
      (
        (a.transaction_merchant_id IS NOT NULL AND a.document_merchant_id IS NOT NULL
         AND a.transaction_merchant_id = a.document_merchant_id)
        OR (
             ((NOT directional_matching) OR a.tx_is_out)
             AND a.tx_merchant_ns IS NOT NULL AND a.doc_merchant_ns IS NOT NULL
             AND (
                  a.tx_merchant_ns = a.doc_merchant_ns
               OR position(a.doc_merchant_ns in a.tx_merchant_ns) > 0
               OR position(a.tx_merchant_ns in a.doc_merchant_ns) > 0
             )
           )
        OR (
             ((NOT directional_matching) OR a.tx_is_out)
             AND a.tx_iban_ns IS NOT NULL AND a.doc_iban_ns IS NOT NULL
             AND a.tx_iban_ns = a.doc_iban_ns
           )
      ) AS merchant_match,

      -- customer: gated by direction if enabled
      (
        ((NOT directional_matching) OR a.tx_is_in)
        AND a.tx_customer_ns IS NOT NULL AND a.doc_customer_ns IS NOT NULL
        AND (
             a.tx_customer_ns = a.doc_customer_ns
          OR position(a.doc_customer_ns in a.tx_customer_ns) > 0
          OR position(a.tx_customer_ns in a.doc_customer_ns) > 0
        )
      ) AS customer_match,

      COALESCE(ABS(a.transaction_date_unix - a.document_date_unix) <= derived_date_window_seconds, FALSE) AS date_match,

      a.document_total,
      a.document_company_id,
      a.transaction_date_unix,
      a.document_date_unix,

      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND a.document_total = a.transaction_debited)  AS amount_exact_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND a.document_total = a.transaction_credited) AS amount_exact_from_credited,
      (a.transaction_debited  IS NOT NULL AND a.transaction_debited  > 0 AND ABS(a.document_total - a.transaction_debited)  <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_debited,
      (a.transaction_credited IS NOT NULL AND a.transaction_credited > 0 AND ABS(a.document_total - a.transaction_credited) <= a.document_total * derived_amount_tolerance_fraction) AS amount_tolerance_from_credited
    FROM attributes a
  ),

  with_amount_tolerance AS (
    SELECT
      e.transaction_id,
      e.document_id,
      e.document_company_id,

      e.invoice_number_match,
      e.order_number_match,
      e.date_match,
      e.currency_match,
      e.merchant_match,
      e.customer_match,

      e.transaction_effective_amount,
      e.document_total,
      e.transaction_date_unix,
      e.document_date_unix,

      COALESCE(e.transaction_effective_amount = e.document_total, FALSE) AS amount_exact_match,

      CASE
        WHEN NOT COALESCE(e.transaction_effective_amount = e.document_total, FALSE)
         AND e.currency_match
         AND e.transaction_effective_amount IS NOT NULL
         AND e.document_total IS NOT NULL
         AND ABS(e.transaction_effective_amount - e.document_total) <= e.document_total * derived_amount_tolerance_fraction
        THEN TRUE
        ELSE FALSE
      END AS amount_tolerance_match,

      (e.amount_exact_from_debited OR e.amount_exact_from_credited OR e.amount_tolerance_from_debited OR e.amount_tolerance_from_credited) AS amount_direction_ok
    FROM evaluated e
  ),

  resolved_weights AS (
    SELECT
      COALESCE((weights_json->>'amount_total')::numeric, 0)   AS amount_weight,
      COALESCE((weights_json->>'invoice_number')::numeric, 0) AS invoice_weight,
      COALESCE((weights_json->>'order_number')::numeric, 0)   AS order_weight,
      COALESCE((weights_json->>'date')::numeric, 0)           AS date_weight,
      COALESCE((weights_json->>'currency')::numeric, 0)       AS currency_weight,
      COALESCE((weights_json->>'merchant_id')::numeric, 0)    AS merchant_weight,
      COALESCE((weights_json->>'customer_name')::numeric, 0)  AS customer_weight
  ),

  scored_components AS (
    SELECT
      m.transaction_id,
      m.document_id,
      m.document_company_id,
      m.transaction_date_unix,
      m.document_date_unix,
      m.amount_direction_ok,

      COALESCE(
        CASE
          WHEN m.amount_exact_match THEN w.amount_weight
          WHEN m.amount_tolerance_match THEN GREATEST(
            0,
            LEAST(
              w.amount_weight,
              w.amount_weight * (1 - (
                ABS(m.transaction_effective_amount - m.document_total)
                / NULLIF(m.document_total * derived_amount_tolerance_fraction, 0)
              ))
            )
          )
          ELSE 0
        END, 0
      )::numeric AS amount_score,

      (m.amount_exact_match OR m.amount_tolerance_match) AS amount_match,
      m.invoice_number_match  AS invoice_match,
      m.order_number_match    AS order_match,
      m.date_match            AS date_match,
      m.currency_match        AS currency_match,
      m.merchant_match        AS merchant_match,
      m.customer_match        AS customer_match,

      w.amount_weight   AS amount_weight,
      w.invoice_weight  AS invoice_weight,
      w.order_weight    AS order_weight,
      w.date_weight     AS date_weight,
      w.currency_weight AS currency_weight,
      w.merchant_weight AS merchant_weight,
      w.customer_weight AS customer_weight,

      (COALESCE(m.invoice_number_match::int,0) * w.invoice_weight)::numeric  AS invoice_score,
      (COALESCE(m.order_number_match::int,0)   * w.order_weight)::numeric    AS order_score,
      (COALESCE(m.date_match::int,0)           * w.date_weight)::numeric     AS date_score,
      (COALESCE(m.currency_match::int,0)       * w.currency_weight)::numeric AS currency_score,
      (COALESCE(m.merchant_match::int,0)       * w.merchant_weight)::numeric AS merchant_score,
      (COALESCE(m.customer_match::int,0)       * w.customer_weight)::numeric AS customer_score,

      m.transaction_effective_amount,
      m.document_total
    FROM with_amount_tolerance m
    CROSS JOIN resolved_weights w
  ),

  scored_with_confidence AS (
    SELECT
      s.transaction_id,
      s.document_id,
      s.document_company_id,
      s.transaction_date_unix,
      s.document_date_unix,
      s.amount_direction_ok,

      (s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score) AS total_numerator,

      (COALESCE(s.amount_match::int,0)
     + COALESCE(s.invoice_match::int,0)
     + COALESCE(s.order_match::int,0)
     + COALESCE(s.date_match::int,0)
     + COALESCE(s.currency_match::int,0)
     + COALESCE(s.merchant_match::int,0)
     + COALESCE(s.customer_match::int,0)) AS signals_matched_count,

      CASE WHEN weights_total_possible > 0 THEN
        ROUND(((s.amount_score + s.invoice_score + s.order_score + s.date_score + s.currency_score + s.merchant_score + s.customer_score)
                / weights_total_possible) * 100.0, 4)
      ELSE 0 END AS confidence_percent,

      s.amount_match,
      s.invoice_match,
      s.order_match,
      s.date_match,
      s.currency_match,
      s.merchant_match,
      s.customer_match,

      s.date_weight,
      s.invoice_weight,
      s.order_weight,
      s.merchant_weight,
      s.amount_weight,
      s.currency_weight,
      s.customer_weight,

      s.date_score,
      s.invoice_score,
      s.order_score,
      s.merchant_score,
      s.amount_score,
      s.currency_score,
      s.customer_score,

      s.transaction_effective_amount,
      s.document_total
    FROM scored_components s
  ),

  -- thresholds and ranking
  low_signal AS (SELECT * FROM scored_with_confidence WHERE signals_matched_count < 1),
  below_min_confidence AS (
    SELECT * FROM scored_with_confidence
    WHERE signals_matched_count >= 1 AND confidence_percent < config_min_confidence
  ),
  base_candidates AS (
    SELECT * FROM scored_with_confidence
    WHERE signals_matched_count >= 1 AND confidence_percent >= config_min_confidence
  ),

  ranked_per_transaction AS (
    SELECT
      b.*,
      ROW_NUMBER() OVER (
        PARTITION BY b.transaction_id
        ORDER BY b.confidence_percent DESC, b.total_numerator DESC, b.signals_matched_count DESC, b.document_id
      ) AS rownum_per_transaction
    FROM base_candidates b
  ),
  not_top_for_transaction AS (SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction > 1),
  top_per_transaction AS (SELECT * FROM ranked_per_transaction WHERE rownum_per_transaction = 1),

  ranked_per_document AS (
    SELECT
      t.*,
      ROW_NUMBER() OVER (
        PARTITION BY t.document_id
        ORDER BY
          t.confidence_percent DESC,
          t.total_numerator DESC,
          t.signals_matched_count DESC,
          ABS(t.transaction_date_unix - t.document_date_unix) ASC,
          t.transaction_id
      ) AS rownum_per_document
    FROM top_per_transaction t
  ),
  not_top_for_document AS (SELECT * FROM ranked_per_document WHERE rownum_per_document > 1),
  top_per_document AS (SELECT * FROM ranked_per_document WHERE rownum_per_document = 1),

  final_candidates AS (
    SELECT *
    FROM top_per_document
    ORDER BY confidence_percent DESC, total_numerator DESC, signals_matched_count DESC, transaction_id, document_id
    LIMIT 2000
  ),

  allowed_for_insert AS (
    SELECT f.*
    FROM   final_candidates f
    WHERE  NOT EXISTS (SELECT 1 FROM public.reconciliations r1 WHERE r1.transaction_id = f.transaction_id)
       AND NOT EXISTS (SELECT 1 FROM public.reconciliations r2 WHERE r2.document_id    = f.document_id)
  ),

  inserted_reconciliations AS (
    INSERT INTO public.reconciliations (
      transaction_id, document_id, company_id, confidence, time,
      engine_version, fiscal_year, quarter, month, summary
    )
    WITH ts_base AS (
      SELECT f.transaction_id, f.document_id, f.document_company_id, f.confidence_percent,
             COALESCE(f.transaction_date_unix, f.document_date_unix, log_run_unix_time)::bigint AS base_unix
      FROM allowed_for_insert f
    ),
    enrich AS (
      SELECT
        d.transaction_id, d.document_id, d.document_company_id, d.confidence_percent,
        d.invoice_match, d.order_match, d.amount_match, d.date_match, d.currency_match, d.merchant_match, d.customer_match,
        d.transaction_effective_amount, d.document_total,
        d.transaction_date_unix, d.document_date_unix,
        t.base_unix,
        a.transaction_invoice_number, a.document_invoice_number,
        a.transaction_order_id, a.document_order_id,
        a.transaction_currency, a.document_currency,
        a.transaction_merchant_id, a.document_merchant_id,
        a.transaction_customer_name, a.document_customer_name
      FROM ts_base t
      JOIN final_candidates d ON d.transaction_id = t.transaction_id AND d.document_id = t.document_id
      JOIN attributes a       ON a.transaction_id = d.transaction_id AND a.document_id = d.document_id
    )
    SELECT
      e.transaction_id, e.document_id, e.document_company_id,
      COALESCE(e.confidence_percent, 0)::numeric,
      log_run_unix_time, engine_version,
      EXTRACT(YEAR FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      EXTRACT(QUARTER FROM (to_timestamp(e.base_unix) AT TIME ZONE 'UTC'))::int,
      to_char((to_timestamp(e.base_unix) AT TIME ZONE 'UTC'), 'FMMonth'),
      json_build_object(
        'signals', (
