create or replace function postgres_fdw_handler() returns fdw_handler
    strict
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_handler() owner to supabase_admin;

grant execute on function postgres_fdw_handler() to postgres;

grant execute on function postgres_fdw_handler() to anon;

grant execute on function postgres_fdw_handler() to authenticated;

grant execute on function postgres_fdw_handler() to service_role;

create or replace function postgres_fdw_validator(text[], oid) returns void
    strict
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_validator(text[], oid) owner to supabase_admin;

grant execute on function postgres_fdw_validator(text[], oid) to postgres;

grant execute on function postgres_fdw_validator(text[], oid) to anon;

grant execute on function postgres_fdw_validator(text[], oid) to authenticated;

grant execute on function postgres_fdw_validator(text[], oid) to service_role;

create or replace function postgres_fdw_get_connections(out server_name text, out valid boolean) returns setof record
    strict
    parallel restricted
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_get_connections(out text, out boolean) owner to supabase_admin;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to postgres;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to anon;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to authenticated;

grant execute on function postgres_fdw_get_connections(out text, out boolean) to service_role;

create or replace function postgres_fdw_disconnect(text) returns boolean
    strict
    parallel restricted
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_disconnect(text) owner to supabase_admin;

grant execute on function postgres_fdw_disconnect(text) to postgres;

grant execute on function postgres_fdw_disconnect(text) to anon;

grant execute on function postgres_fdw_disconnect(text) to authenticated;

grant execute on function postgres_fdw_disconnect(text) to service_role;

create or replace function postgres_fdw_disconnect_all() returns boolean
    strict
    parallel restricted
    language c
as
$$
begin
-- missing source code
end;
$$;

alter function postgres_fdw_disconnect_all() owner to supabase_admin;

grant execute on function postgres_fdw_disconnect_all() to postgres;

grant execute on function postgres_fdw_disconnect_all() to anon;

grant execute on function postgres_fdw_disconnect_all() to authenticated;

grant execute on function postgres_fdw_disconnect_all() to service_role;

create or replace function fetch_sessions()
    returns TABLE(ip text, user_id uuid, refreshed_at timestamp without time zone, created_at timestamp without time zone)
    language plpgsql
as
$$
BEGIN
    RETURN QUERY
    SELECT s.ip, s.user_id, s.refreshed_at, s.created_at
    FROM auth.sessions s;  -- Use table alias 's' for the sessions table
END;
$$;

alter function fetch_sessions() owner to postgres;

grant execute on function fetch_sessions() to anon;

grant execute on function fetch_sessions() to authenticated;

grant execute on function fetch_sessions() to service_role;

create or replace function copy_sessions_to_public() returns void
    language plpgsql
as
$$
BEGIN
    DELETE FROM public.sessions;
    -- Insert data from auth.sessions to public.sessions
    INSERT INTO public.sessions (
        session_id,      -- Mapping auth.sessions.id to public.sessions.session_id
        created_at, 
        user_id, 
        updated_at, 
        factor_id, 
        aal, 
        user_agent, 
        ip, 
        tag, 
        refreshed_at, 
        not_after
    )
    SELECT
        id,  -- Use the id from auth.sessions as session_id in public.sessions
        created_at,
        user_id,
        updated_at,
        NULL,  -- Assuming factor_id is not available in auth.sessions
        NULL,  -- Assuming aal is not available in auth.sessions
        user_agent,  
        ip,
        NULL,  -- Assuming tag is not available in auth.sessions
        refreshed_at,
        NULL   -- Assuming not_after is not available in auth.sessions
    FROM auth.sessions;
END;
$$;

alter function copy_sessions_to_public() owner to postgres;

grant execute on function copy_sessions_to_public() to anon;

grant execute on function copy_sessions_to_public() to authenticated;

grant execute on function copy_sessions_to_public() to service_role;

create or replace function run_sql(query text) returns jsonb
    security definer
    language plpgsql
as
$$
declare
  result jsonb;
begin
  -- wrap the user’s SQL in a sub-select so we can return a single JSON blob
  execute format(
    'select jsonb_agg(t) from (%s) as t',
    query
  ) into result;
  return coalesce(result, '[]'::jsonb); -- ← changed from json to jsonb (or add a comment)
end;
$$;

alter function run_sql(unknown) owner to postgres;

grant execute on function run_sql(unknown) to anon;

grant execute on function run_sql(unknown) to authenticated;

grant execute on function run_sql(unknown) to service_role;

create or replace function pg_schema()
    returns TABLE(table_name text)
    language sql
as
$$
  SELECT table_name
  FROM information_schema.tables
  WHERE table_schema = 'public'
    AND table_type = 'BASE TABLE';
$$;

alter function pg_schema() owner to postgres;

grant execute on function pg_schema() to anon;

grant execute on function pg_schema() to authenticated;

grant execute on function pg_schema() to service_role;

create or replace function execute_sql(sql text) returns void
    security definer
    language plpgsql
as
$$
BEGIN
  EXECUTE sql;
END;
$$;

alter function execute_sql(unknown) owner to postgres;

grant execute on function execute_sql(unknown) to anon;

grant execute on function execute_sql(unknown) to authenticated;

grant execute on function execute_sql(unknown) to service_role;

create or replace function reconcile_now(p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_company_id uuid DEFAULT NULL::uuid) returns integer
    language plpgsql
as
$$
DECLARE
  v_has_unique boolean;
  v_sql text;
  v_rows integer := 0;
BEGIN
  -- Detect if there is a UNIQUE index/constraint on (company_id, transaction_id, document_id)
  SELECT EXISTS (
    SELECT 1
    FROM pg_indexes
    WHERE schemaname = 'public'
      AND tablename  = 'reconciliations'
      AND indexdef ILIKE '%UNIQUE%' 
      AND (
        indexdef ILIKE '%(company_id, transaction_id, document_id)%'
        OR indexdef ILIKE '%(company_id,transaction_id,document_id)%'
      )
  ) INTO v_has_unique;

  IF v_has_unique THEN
    -- Fast path: ON CONFLICT (requires a unique index/constraint)
    v_sql := $f$
      INSERT INTO public.reconciliations (
        athena_view,
        transaction_id,
        document_id,
        company_id
      )
      SELECT
        $1::text                           AS athena_view,
        t.transaction_id,
        d.document_id,
        t.company_id
      FROM public.transactions t
      JOIN public.documents_v2 d
        ON d.total = t.debited
       AND COALESCE(d.currency, t.currency) = t.currency
       AND d.company_id = t.company_id
      WHERE ($2::uuid IS NULL OR t.company_id = $2::uuid)
      ON CONFLICT (company_id, transaction_id, document_id) DO NOTHING
    $f$;

    EXECUTE v_sql USING p_athena_view, p_company_id;
    GET DIAGNOSTICS v_rows = ROW_COUNT;

  ELSE
    -- Safe path: anti-join without requiring schema changes
    v_sql := $f$
      WITH src AS (
        SELECT
          t.transaction_id,
          d.document_id,
          t.company_id
        FROM public.transactions t
        JOIN public.documents_v2 d
          ON d.total = t.debited
         AND COALESCE(d.currency, t.currency) = t.currency
         AND d.company_id = t.company_id
        WHERE ($2::uuid IS NULL OR t.company_id = $2::uuid)
      )
      INSERT INTO public.reconciliations (
        athena_view,
        transaction_id,
        document_id,
        company_id
      )
      SELECT
        $1::text,
        s.transaction_id,
        s.document_id,
        s.company_id
      FROM src s
      WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id    = s.company_id
          AND r.transaction_id = s.transaction_id
          AND r.document_id    = s.document_id
      )
    $f$;

    EXECUTE v_sql USING p_athena_view, p_company_id;
    GET DIAGNOSTICS v_rows = ROW_COUNT;
  END IF;

  RETURN v_rows;
END;
$$;

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

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

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

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

create or replace function list_views()
    returns TABLE(table_schema text, table_name text)
    stable
    language sql
as
$$
  SELECT table_schema, table_name
  FROM information_schema.views
  WHERE table_schema NOT in ('pg_catalog', 'information_schema')
  ORDER BY table_schema, table_name;
$$;

alter function list_views() owner to postgres;

grant execute on function list_views() to anon;

grant execute on function list_views() to authenticated;

grant execute on function list_views() to service_role;

create or replace function list_extensions()
    returns TABLE(ext_name text, ext_comment text, default_version text, installed_version text)
    stable
    language sql
as
$$
  SELECT
    e.name,
    e.comment,
    e.default_version,
    e.installed_version
  FROM pg_available_extensions e
  ORDER BY e.name ASC;
$$;

alter function list_extensions() owner to postgres;

grant execute on function list_extensions() to anon;

grant execute on function list_extensions() to authenticated;

grant execute on function list_extensions() to service_role;

create or replace function schema(p_table_name text, p_schema_name text DEFAULT 'public'::text) returns jsonb
    language plpgsql
as
$$
BEGIN
  RETURN (
    SELECT jsonb_object_agg(
             column_name,
             jsonb_build_object(
               'is_nullable', (is_nullable = 'YES'),
               'type', udt_name
             )
           )
    FROM information_schema.columns
    WHERE table_schema = p_schema_name
      AND table_name = p_table_name
  );
END;
$$;

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

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

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

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

create or replace function sync_sg_companies_to_global() returns void
    language plpgsql
as
$$
begin
  insert into public.global_companies_registry (
    name,
    company_number,
    country_code,
    status,
    establishment_date,
    entity_type,
    description,
    postal_code,
    street,
    house_number,
    city,
    state,
    region,
    last_update
  )
  select
    sc.entity_name as name,
    sc.uen as company_number,
    'SG' as country_code,
    sc.entity_status_description as status,
    sc.registration_incorporation_date as establishment_date,
    sc.entity_type_description as entity_type,
    sc.primary_user_described_activity as description,
    sc.postal_code,
    sc.street_name as street,
    sc.block as house_number,
    sc.building_name as city,
    sc.level_no as state,
    sc.unit_no as region,
    now()::text as last_update
  from public.sg_companies sc
  on conflict (hash) do nothing;
end;
$$;

alter function sync_sg_companies_to_global() owner to postgres;

grant execute on function sync_sg_companies_to_global() to anon;

grant execute on function sync_sg_companies_to_global() to authenticated;

grant execute on function sync_sg_companies_to_global() to service_role;

create or replace function stage_reconciliations() returns jsonb
    language plpgsql
as
$$
DECLARE 
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end TIMESTAMPTZ;
    v_ms BIGINT;
    v_sql TEXT;
BEGIN
    -- Build insert SQL (keep same structure as your v2 reconcile function)
    v_sql := $f$
        INSERT INTO public.reconciliations_staging (reconciliation_id)
        SELECT r.reconciliation_id
        FROM public.reconciliations r
        -- optional: avoid duplicates if staging already has them
        WHERE NOT EXISTS (
            SELECT 1
            FROM public.reconciliations_staging s
            WHERE s.reconciliation_id = r.reconciliation_id
        )
    $f$;

    BEGIN
        EXECUTE v_sql;
        GET DIAGNOSTICS v_rows = ROW_COUNT;
    EXCEPTION
        WHEN OTHERS THEN
            v_status := 'error';
            v_msg := SQLERRM;
    END;

    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_staged', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

alter function stage_reconciliations() owner to postgres;

grant execute on function stage_reconciliations() to anon;

grant execute on function stage_reconciliations() to authenticated;

grant execute on function stage_reconciliations() to service_role;

create or replace function athena_reconcile_v2(p_athena_view text DEFAULT 'v_reconciled_totals'::text) returns jsonb
    language plpgsql
as
$$
declare 
    v_has_unique boolean;
    v_sql text;
    v_rows integer := 0;
    v_status text := 'ok';
    v_msg text := 'success';
    v_start timestamptz := clock_timestamp();
    v_end timestamptz;
    v_ms bigint;
begin
    -- ensure pgcrypto exists
    begin
        create extension if not exists pgcrypto;
    exception
        when insufficient_privilege or duplicate_object then null;
    end;

    -- check if unique constraint exists
    select exists (
        select 1
        from pg_constraint c
        join pg_class t on t.oid = c.conrelid and t.relname = 'reconciliations'
        join pg_namespace n on n.oid = t.relnamespace and n.nspname = 'public'
        where c.contype = 'u'
          and (
              select array_agg(attname::text order by attnum)
              from pg_attribute
              where attrelid = t.oid
                and attnum = any (c.conkey)
          ) = array['company_id','transaction_id','document_id']::text[]
    ) into v_has_unique;

    if v_has_unique then
        v_sql := $f$
            with tx as (
                select t.transaction_id,
                       t.company_id,
                       t.currency,
                       nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric as debited_num
                from public.transactions t
                where nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '') is not null
            )
            insert into public.reconciliations (
                athena_view,
                transaction_id,
                document_id,
                company_id
            )
            select $1::text,
                   tx.transaction_id::uuid,
                   d.document_id::uuid,
                   tx.company_id::uuid
            from tx
            join public.documents_v2 d
              on round(d.total::numeric, 2) = round(tx.debited_num, 2)
             and (d.currency is null or upper(d.currency) = upper(tx.currency))
            where tx.company_id::uuid = d.company_id::uuid
            on conflict (company_id, transaction_id, document_id) do nothing
        $f$;
    else
        v_sql := $f$
            with tx as (
                select t.transaction_id,
                       t.company_id,
                       t.currency,
                       nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric as debited_num
                from public.transactions t
                where nullif(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '') is not null
            ),
            src as (
                select tx.transaction_id,
                       d.document_id,
                       tx.company_id
                from tx
                join public.documents_v2 d
                  on round(d.total::numeric, 2) = round(tx.debited_num, 2)
                 and (d.currency is null or upper(d.currency) = upper(tx.currency))
                where tx.company_id::uuid = d.company_id::uuid
            )
            insert into public.reconciliations (
                athena_view,
                transaction_id,
                document_id,
                company_id
            )
            select $1::text,
                   s.transaction_id::uuid,
                   s.document_id::uuid,
                   s.company_id::uuid
            from src s
            where not exists (
                select 1
                from public.reconciliations r
                where r.company_id = s.company_id::uuid
                  and r.transaction_id = s.transaction_id::uuid
                  and r.document_id = s.document_id::uuid
            )
        $f$;
    end if;

    begin
        execute v_sql using p_athena_view;
        get diagnostics v_rows = row_count;
    exception
        when others then
            v_status := 'error';
            v_msg := sqlerrm;
    end;

    v_end := clock_timestamp();
    v_ms := floor(extract(epoch from (v_end - v_start)) * 1000)::bigint;

    return jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
end;
$$;

alter function athena_reconcile_v2(unknown) owner to postgres;

grant execute on function athena_reconcile_v2(unknown) to anon;

grant execute on function athena_reconcile_v2(unknown) to authenticated;

grant execute on function athena_reconcile_v2(unknown) to service_role;

create or replace function fetch_and_cache_link_meta(p_url text) returns bigint
    language plpgsql
as
$$
DECLARE
    resp json;
    title text;
    description text;
    images json := '[]'::json;
    domain text;
    content_type text;
    cache_id bigint;
BEGIN
    -- Fetch URL
    resp := (SELECT content::json
             FROM http_get(p_url, 'application/json', ARRAY[http_header('User-Agent','Mozilla/5.0')]));

    -- Extract domain
    domain := split_part(p_url, '/', 3);

    -- Try naive title parsing (depends on response type)
    title := (regexp_match(resp::text, '<title>(.*?)</title>'))[1];
    description := (regexp_match(resp::text, '<meta name="description" content="(.*?)"'))[1];

    -- Insert into cache table
    INSERT INTO public.link_meta_preview_cache (
        url, title, description, content_type, domain
    )
    VALUES (
        p_url, title, description, content_type, domain
    )
    RETURNING id INTO cache_id;

    RETURN cache_id;
EXCEPTION
    WHEN others THEN
        RAISE NOTICE 'Error fetching/parsing %: %', p_url, SQLERRM;
        RETURN NULL;
END;
$$;

alter function fetch_and_cache_link_meta(unknown) owner to postgres;

grant execute on function fetch_and_cache_link_meta(unknown) to anon;

grant execute on function fetch_and_cache_link_meta(unknown) to authenticated;

grant execute on function fetch_and_cache_link_meta(unknown) to service_role;

create or replace function athena_reconcile_v5(p_athena_view text DEFAULT 'v_reconciled_totals'::text) returns jsonb
    language plpgsql
as
$$
DECLARE 
    v_has_unique BOOLEAN;
    v_sql TEXT;               -- holds the INSERT body (without RETURNING wrapper)
    v_rows INTEGER := 0;      -- rows inserted
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end TIMESTAMPTZ;
    v_ms BIGINT;
BEGIN 
    -- ensure pgcrypto exists (no-op if lacks privilege)
    BEGIN 
        CREATE EXTENSION IF NOT EXISTS pgcrypto;
    EXCEPTION
        WHEN insufficient_privilege OR duplicate_object THEN NULL;
    END;

    -- Detect unique constraint on (company_id, transaction_id, document_id)
    SELECT EXISTS (
        SELECT 1
        FROM pg_constraint c
        JOIN pg_class t ON t.oid = c.conrelid AND t.relname = 'reconciliations'
        JOIN pg_namespace n ON n.oid = t.relnamespace AND n.nspname = 'public'
        WHERE c.contype = 'u'
          AND (
              SELECT array_agg(attname::TEXT ORDER BY attnum)
              FROM pg_attribute
              WHERE attrelid = t.oid
                AND attnum = ANY (c.conkey)
          ) = ARRAY['company_id','transaction_id','document_id']::TEXT[]
    ) INTO v_has_unique;

    IF v_has_unique THEN 
        v_sql := $f$
WITH tx AS (
    SELECT
        t.transaction_id,
        t.company_id,
        t.currency,
        t.fiscal_year,
        t.quarter,
        t.month,
        NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '')::NUMERIC AS debited_num
    FROM public.transactions t
    WHERE NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '') IS NOT NULL
      AND COALESCE(t.reconciled, FALSE) = FALSE -- ignore reconciled transactions
)
INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, fiscal_year, quarter, month
)
SELECT
    $1::TEXT,
    tx.transaction_id::uuid,
    d.document_id::uuid,
    tx.company_id::uuid,
    tx.fiscal_year,
    tx.quarter,
    tx.month
FROM tx
JOIN public.documents_v2 d
  ON ROUND(d.total::NUMERIC, 2) = ROUND(tx.debited_num, 2)
 AND (d.currency IS NULL OR UPPER(d.currency) = UPPER(tx.currency))
 AND COALESCE(d.reconciled, FALSE) = FALSE -- ignore reconciled documents
WHERE tx.company_id::uuid = d.company_id::uuid
ON CONFLICT (company_id, transaction_id, document_id) DO NOTHING
$f$;

    ELSE 
        v_sql := $f$
WITH tx AS (
    SELECT
        t.transaction_id,
        t.company_id,
        t.currency,
        t.fiscal_year,
        t.quarter,
        t.month,
        NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '')::NUMERIC AS debited_num
    FROM public.transactions t
    WHERE NULLIF(regexp_replace(t.debited::TEXT, '[^\d\.-]', '', 'g'), '') IS NOT NULL
      AND COALESCE(t.reconciled, FALSE) = FALSE -- ignore reconciled transactions
),
src AS (
    SELECT
        tx.transaction_id,
        d.document_id,
        tx.company_id,
        tx.fiscal_year,
        tx.quarter,
        tx.month
    FROM tx
    JOIN public.documents_v2 d
      ON ROUND(d.total::NUMERIC, 2) = ROUND(tx.debited_num, 2)
     AND (d.currency IS NULL OR UPPER(d.currency) = UPPER(tx.currency))
     AND COALESCE(d.reconciled, FALSE) = FALSE -- ignore reconciled documents
    WHERE tx.company_id::uuid = d.company_id::uuid
)
INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, fiscal_year, quarter, month
)
SELECT
    $1::TEXT,
    s.transaction_id::uuid,
    s.document_id::uuid,
    s.company_id::uuid,
    s.fiscal_year,
    s.quarter,
    s.month
FROM src s
WHERE NOT EXISTS (
    SELECT 1
    FROM public.reconciliations r
    WHERE r.company_id     = s.company_id::uuid
      AND r.transaction_id = s.transaction_id::uuid
      AND r.document_id    = s.document_id::uuid
)
$f$;
    END IF;

    -- Execute the INSERT and stamp processing_time on the newly inserted rows.
    -- We avoid assuming a reconciliation_id column exists by joining back on the unique key.
    BEGIN
        EXECUTE
            'WITH ins AS (' || v_sql || ' RETURNING company_id, transaction_id, document_id ), ' ||
            'upd AS ( ' ||
            '  UPDATE public.reconciliations r ' ||
            '     SET processing_time = $2 ' ||
            '    FROM ins i ' ||
            '   WHERE r.company_id = i.company_id ' ||
            '     AND r.transaction_id = i.transaction_id ' ||
            '     AND r.document_id = i.document_id ' ||
            '  RETURNING 1 ' ||
            ') ' ||
            'SELECT count(*) FROM ins'
        USING p_athena_view, v_ms
        INTO v_rows;
    EXCEPTION
        WHEN OTHERS THEN 
            v_status := 'error';
            v_msg := SQLERRM;
    END;

    -- Finish timing
    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

alter function athena_reconcile_v5(unknown) owner to postgres;

grant execute on function athena_reconcile_v5(unknown) to anon;

grant execute on function athena_reconcile_v5(unknown) to authenticated;

grant execute on function athena_reconcile_v5(unknown) to service_role;

create or replace function stage_reconciliations_v2() returns jsonb
    language plpgsql
as
$$
DECLARE 
  v_rows    integer := 0;
  v_status  text    := 'ok';
  v_msg     text    := 'success';
  v_start   timestamptz := clock_timestamp();
  v_end     timestamptz;
  v_ms      bigint;
  v_sql     text;
  v_run_id  uuid;  -- logging run id
BEGIN
  -- start logging
  v_run_id := public.reconciliations_log_start(
    p_function_version => 'stage_reconciliations_v2',
    p_company_id       => NULL,
    p_context          => jsonb_build_object('target_table','reconciliations_staging')
  );

  -- Build insert SQL (includes company_id)
  v_sql := $f$
    INSERT INTO public.reconciliations_staging (reconciliation_id, company_id)
    SELECT r.reconciliation_id, r.company_id
    FROM public.reconciliations r
    WHERE NOT EXISTS (
      SELECT 1
      FROM public.reconciliations_staging s
      WHERE s.reconciliation_id = r.reconciliation_id
    )
  $f$;

  BEGIN
    EXECUTE v_sql;
    GET DIAGNOSTICS v_rows = ROW_COUNT;

    -- step log (success of insert)
    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'stage_reconciliations_v2',
      p_action            => 'staged'::text,
      p_status            => 'ok'::text,
      p_target            => 'reconciliations_staging'::text,
      p_message           => NULL::text,
      p_company_id        => NULL::uuid,
      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           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_start)))::bigint),
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('note','rows inserted into staging')
    );

  EXCEPTION WHEN OTHERS THEN
    v_status := 'error';
    v_msg := SQLERRM;

    -- step log (error during insert)
    PERFORM public.reconciliations_log_step(
      p_run_id            => v_run_id,
      p_function_version  => 'stage_reconciliations_v2',
      p_action            => 'staged'::text,
      p_status            => 'error'::text,
      p_target            => 'reconciliations_staging'::text,
      p_message           => v_msg,
      p_company_id        => NULL::uuid,
      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           => (ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - v_start)))::bigint),
      p_processing_ms     => NULL::bigint,
      p_context           => jsonb_build_object('hint','see SQL error above')
    );
  END;

  v_end := clock_timestamp();
  v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::bigint;

  -- end log
  PERFORM public.reconciliations_log_end(
    p_run_id            => v_run_id,
    p_function_version  => 'stage_reconciliations_v2',
    p_status            => (CASE WHEN v_status = 'ok' THEN 'ok' ELSE 'error' END)::text,
    p_message           => (CASE WHEN v_status = 'ok' THEN NULL ELSE v_msg END),
    p_processing_ms     => v_ms,
    p_context           => jsonb_build_object('reconciliations_staged', v_rows::int)
  );

  RETURN jsonb_build_object(
    'reconciliations_staged', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', v_ms
  );
END;
$$;

alter function stage_reconciliations_v2() owner to postgres;

grant execute on function stage_reconciliations_v2() to anon;

grant execute on function stage_reconciliations_v2() to authenticated;

grant execute on function stage_reconciliations_v2() to service_role;

create or replace function create_notification(p_table_name text, p_action text, p_resource_id text, p_resource_ref text, p_company_id uuid, p_user_id text DEFAULT NULL::text) returns void
    language plpgsql
as
$$
declare
  notif_type record;
  user_pref record;
  global_enabled boolean;
begin
  -- 0. Check admin_config toggle
  select enable_notification_generation
  into global_enabled
  from admin_config
  limit 1;

  if not found or global_enabled = false then
    insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, message)
    values ('ERROR', p_action, p_table_name, p_resource_ref, p_company_id, p_user_id, 'Notification generation disabled in admin_config');
    return;
  end if;

  -- 1. Find matching notification type
  select *
  into notif_type
  from notification_types
  where action = p_action
    and resource_type = p_table_name
    and enabled = true
  limit 1;

  if not found then
    insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, message)
    values ('ERROR', p_action, p_table_name, p_resource_ref, p_company_id, p_user_id, 'Notification type not found or disabled');
    return;
  end if;

  -- 2. Loop through eligible users
  for user_pref in
    select *
    from notification_settings
    where company_id = p_company_id
      and enabled = true
  loop
    begin
      -- 3. Insert notification
  insert into notifications (
    id,
    user_id,
    company_id,
    title,
    message,
    type,
    action,
    target,
    system_message
  ) values (
    nextval(pg_get_serial_sequence('notifications','id')),  -- force safe sequence
    user_pref.user_id,
    p_company_id,
    notif_type.title,
    notif_type.description,
    notif_type.resource_type,
    p_action,
    p_resource_ref,
    true
  );


      -- 4. Log success
      insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, notification_type_id, message)
      values ('SUCCESS', p_action, p_table_name, p_resource_ref, p_company_id, user_pref.user_id, notif_type.id, 'Notification created successfully');

    exception when others then
      -- 5. Log error if insert fails
      insert into notification_logs (status, action, resource_type, resource_ref, company_id, user_id, notification_type_id, message)
      values ('ERROR', p_action, p_table_name, p_resource_ref, p_company_id, user_pref.user_id, notif_type.id, SQLERRM);
    end;
  end loop;
end;
$$;

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

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

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

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

create or replace function generate_create_table(p_table_name text, p_schema_name text DEFAULT 'public'::text) returns text
    language plpgsql
as
$$
DECLARE
    v_sql         text;
    v_cols        text;
    v_constraints text;
BEGIN
    -- ensure table exists
    IF NOT EXISTS (
        SELECT 1
        FROM information_schema.tables
        WHERE table_schema = p_schema_name
          AND table_name   = p_table_name
    ) THEN
        RAISE EXCEPTION 'Table %.% does not exist', p_schema_name, p_table_name;
    END IF;

    -- Build column definitions
    SELECT string_agg(
               '    ' || quote_ident(c.column_name) || ' ' ||
               -- data type with length/precision when applicable
               (
                   CASE
                       WHEN c.data_type IN ('character varying','character','bit','bit varying')
                            AND c.character_maximum_length IS NOT NULL
                           THEN c.data_type || '(' || c.character_maximum_length || ')'
                       WHEN c.data_type IN ('numeric','decimal')
                            AND c.numeric_precision IS NOT NULL
                           THEN c.data_type || '(' || c.numeric_precision ||
                                COALESCE(',' || c.numeric_scale, '') || ')'
                       ELSE c.data_type
                   END
               ) ||
               -- identity handling
               CASE
                   WHEN c.is_identity = 'YES'
                       THEN ' GENERATED ' || c.identity_generation || ' AS IDENTITY'
                   ELSE ''
               END ||
               -- default value (skip for identity)
               CASE
                   WHEN c.column_default IS NOT NULL AND c.is_identity = 'NO'
                       THEN ' DEFAULT ' || c.column_default
                   ELSE ''
               END ||
               -- nullability
               CASE
                   WHEN c.is_nullable = 'NO'
                       THEN ' NOT NULL'
                   ELSE ''
               END
               , E',\n'
               ORDER BY c.ordinal_position
           )
    INTO v_cols
    FROM information_schema.columns c
    WHERE c.table_schema = p_schema_name
      AND c.table_name   = p_table_name;

    -- Build constraints (PRIMARY KEY, UNIQUE) without nested aggregates
    SELECT string_agg(
               '    CONSTRAINT ' || quote_ident(tc.constraint_name) || ' ' ||
               CASE tc.constraint_type
                   WHEN 'PRIMARY KEY' THEN 'PRIMARY KEY'
                   WHEN 'UNIQUE'      THEN 'UNIQUE'
               END || ' (' ||
               (
                   SELECT string_agg(quote_ident(k2.column_name), ', ' ORDER BY k2.ordinal_position)
                   FROM information_schema.key_column_usage k2
                   WHERE k2.constraint_name = tc.constraint_name
                     AND k2.table_schema    = tc.table_schema
                     AND k2.table_name      = tc.table_name
               )
               || ')'
               , E',\n'
               ORDER BY tc.constraint_name
           )
    INTO v_constraints
    FROM information_schema.table_constraints tc
    WHERE tc.table_schema   = p_schema_name
      AND tc.table_name     = p_table_name
      AND tc.constraint_type IN ('PRIMARY KEY','UNIQUE');

    -- Assemble full SQL
    v_sql := 'CREATE TABLE '
             || quote_ident(p_schema_name) || '.' || quote_ident(p_table_name) || E' (\n'
             || v_cols
             || COALESCE(E',\n' || v_constraints, '')
             || E'\n);\n';

    RETURN v_sql;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v1(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_date_window_days integer DEFAULT 7, p_amount_tolerance numeric DEFAULT 0.01, p_require_currency boolean DEFAULT false, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),
/* --------------------- SOURCE NORMALIZATION --------------------- */
tx AS (
    SELECT
        t.transaction_id::uuid                 AS transaction_id,
        t.company_id::text                     AS company_id_txt,     -- TEXT in transactions
        t.completed_date::bigint               AS completed_date,     -- unix seconds
        UPPER(NULLIF(t.currency, ''))          AS currency_norm_tx,
        NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,    -- compare as TEXT
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,    -- UUID in documents_v2
        /* Robust TEXT -> unix seconds conversion (handles '', 10/13-digit unix, ISO-ish, and DD/MM/YYYY) */
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            /* ISO-like: 2025-08-30[ T]12:34[:56][Z|+/-..] */
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            /* DD/MM/YYYY (common in IE); safe only when day > 12 to avoid ambiguity */
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            /* MM/DD/YYYY (fallback when month <= 12 and day <= 12) */
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,      -- unix seconds
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,    -- compare as TEXT
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),
/* --------------------- CANDIDATES (must match at least one field) --------------------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text    -- TEXT = TEXT
     AND (
            ABS(tx.debited_num - docs.total_num) <= $4
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix  IS NOT NULL AND ABS((docs.document_unix - tx.completed_date) / 86400) <= $3)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
     AND (
            ($5 = FALSE)  -- optional hard requirement for currency equality
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),
/* --------------------- FIELD MATCH FLAGS --------------------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ABS(c.debited_num - c.total_num) <= $4) 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) <= $3) AS date_match
    FROM candidates c
),
/* --------------------- WEIGHTED SCORING --------------------- */
weighted AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        /* numerator: sum of weights for matched fields */
        (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
        (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
        (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
        (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
        (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
        (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END) AS numerator,
        /* denominator: only fields present on both sides count */
        (CASE WHEN s.document_unix IS NOT NULL AND s.completed_date IS NOT NULL THEN w.date           ELSE 0 END) +
        (CASE WHEN s.doc_invoice  IS NOT NULL AND s.tx_invoice  IS NOT NULL     THEN w.invoice_number ELSE 0 END) +
        (CASE WHEN s.doc_order    IS NOT NULL AND s.tx_order    IS NOT NULL     THEN w.order_number   ELSE 0 END) +
        (CASE WHEN s.doc_merchant IS NOT NULL AND s.tx_merchant IS NOT NULL     THEN w.merchant_id    ELSE 0 END) +
        (CASE WHEN s.debited_num  IS NOT NULL AND s.total_num   IS NOT NULL     THEN w.amount_total   ELSE 0 END) +
        (CASE WHEN s.currency_norm_tx IS NOT NULL AND s.currency_norm IS NOT NULL THEN w.currency     ELSE 0 END) AS denominator,
        /* tie-break helpers */
        ABS(COALESCE(s.debited_num,0) - COALESCE(s.total_num,0))                    AS amount_diff_abs,
        ABS(COALESCE((s.document_unix - s.completed_date) / 86400, 1000000))::int   AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),
conf AS (
    SELECT
        w.*,
        CASE WHEN w.denominator > 0
             THEN ROUND(100.0 * w.numerator / w.denominator, 2)
             ELSE 0 END AS confidence
    FROM weighted w
),
/* --------------------- PICK BEST DOC PER TX --------------------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence,
        c.amount_diff_abs,
        c.date_diff_days
    FROM conf c
    WHERE c.confidence >= $6
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             amount_diff_abs ASC,
             date_diff_days ASC,
             document_id
),
/* --------------------- INSERT (NO ON CONFLICT; dedupe via NOT EXISTS) --------------------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,     -- insert as UUID from documents_v2
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    -- Execute with params in the same order as placeholders
    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_amount_tolerance, -- $4
            p_require_currency, -- $5
            p_min_confidence    -- $6
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

    -- Timing
    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v2(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_date_window_days integer DEFAULT 7, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *,
           (date + invoice_number + order_number + merchant_id + amount_total + currency) AS total_weight
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),

/* --------- SOURCE NORMALIZATION --------- */
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,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* --------- CANDIDATES: company AND at least one field --------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text
     AND (
            ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2)
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
             AND ABS((docs.document_unix - tx.completed_date) / 86400) <= $3)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),

/* --------- MATCH FLAGS --------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) 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) <= $3) AS date_match
    FROM candidates c
),

/* --------- CONFIDENCE (fixed denominator = total weight) --------- */
conf AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        w.total_weight,
        w.currency AS currency_weight,
        /* numerator once, reuse for confidence and tie-break */
        (
          (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
          (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
          (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
          (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
          (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
          (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
        ) AS numerator,
        CASE WHEN w.total_weight > 0
             THEN ROUND(100.0 * (
                    (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
                    (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
                    (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
                    (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
                    (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
                    (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
                  ) / w.total_weight, 2)
             ELSE 0 END AS confidence,
        ABS(COALESCE((s.document_unix - s.completed_date) / 86400, 1000000))::int AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),

/* --------- PICK BEST DOC PER TX --------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence
    FROM conf c
    WHERE c.confidence >= $4
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             /* weak nudge when only currency is missing */
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
),

/* --------- INSERT (dedupe via NOT EXISTS) --------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v3(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'v_reconciled_totals'::text, p_date_window_days integer DEFAULT 7, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *,
           (date + invoice_number + order_number + merchant_id + amount_total + currency) AS total_weight
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),

/* --------- SOURCE NORMALIZATION --------- */
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,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* --------- CANDIDATES: company AND at least one field --------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text
     AND (
            ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2)
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
             AND ABS((docs.document_unix - tx.completed_date) / 86400) <= $3)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),

/* --------- MATCH FLAGS --------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) 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) <= $3) AS date_match
    FROM candidates c
),

/* --------- CONFIDENCE (fixed denominator = total weight) --------- */
conf AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        w.total_weight,
        w.currency AS currency_weight,
        /* numerator once, reuse for confidence and tie-break */
        (
          (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
          (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
          (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
          (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
          (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
          (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
        ) AS numerator,
        CASE WHEN w.total_weight > 0
             THEN ROUND(100.0 * (
                    (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
                    (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
                    (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
                    (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
                    (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
                    (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
                  ) / w.total_weight, 2)
             ELSE 0 END AS confidence,
        ABS(COALESCE((s.document_unix - s.completed_date) / 86400, 1000000))::int AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),

/* --------- PICK BEST DOC PER TX --------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence
    FROM conf c
    WHERE c.confidence >= $4
    ORDER BY company_id_txt, transaction_id,
             confidence DESC,
             /* weak nudge when only currency is missing */
             (CASE WHEN c.numerator >= (c.total_weight - c.currency_weight) THEN 1 ELSE 0 END) DESC,
             date_diff_days ASC,
             document_id
),

/* --------- INSERT (dedupe via NOT EXISTS) --------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v4(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v4'::text, p_date_window_days integer DEFAULT 7, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH weights AS (
    SELECT *,
           (date + invoice_number + order_number + merchant_id + amount_total + currency) AS total_weight
    FROM public.reconciliations_confidence_weights
    WHERE ($1 IS NULL OR weightset_id = $1)
    ORDER BY weightset_id
    LIMIT 1
),

/* --------- SOURCE NORMALIZATION --------- */
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,     -- unix seconds
        UPPER(NULLIF(t.currency, ''))          AS currency_norm_tx,
        NULLIF(regexp_replace(t.debited::text, '[^\d\.-]', '', 'g'), '')::numeric AS debited_num,
        UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        t.merchant_id::text                    AS merchant_id_txt,
        COALESCE(t.reconciled, FALSE)          AS reconciled
    FROM public.transactions t
    WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
    SELECT
        d.document_id::uuid                    AS document_id,
        d.company_id::uuid                     AS company_id_uuid,
        CASE
            WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
            WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
            WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
            WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
              THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
                 AND split_part(btrim(d.document_date), '/', 1)::int > 12
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
            WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
              THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
            ELSE NULL
        END::bigint                            AS document_unix,      -- unix seconds
        UPPER(NULLIF(d.currency, ''))          AS currency_norm,
        ROUND(d.total::numeric, 2)             AS total_num,
        UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
        UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
        d.merchant_id::text                    AS merchant_id_txt,
        COALESCE(d.reconciled, FALSE)          AS reconciled
    FROM public.documents_v2 d
    WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* --------- CANDIDATES: company AND at least one field --------- */
candidates AS (
    SELECT
        tx.transaction_id,
        docs.document_id,
        tx.company_id_txt,
        docs.company_id_uuid,
        tx.completed_date,
        docs.document_unix,
        tx.currency_norm_tx,
        docs.currency_norm,
        tx.debited_num,
        docs.total_num,
        tx.invoice_nr_norm AS tx_invoice,
        docs.invoice_nr_norm AS doc_invoice,
        tx.order_id_norm AS tx_order,
        docs.order_id_norm AS doc_order,
        tx.merchant_id_txt  AS tx_merchant,
        docs.merchant_id_txt AS doc_merchant
    FROM tx
    JOIN docs
      ON tx.company_id_txt = docs.company_id_uuid::text
     AND (
            ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2)
         OR (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm)
         OR (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm)
         OR (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt)
         OR (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
             AND FLOOR(docs.document_unix / 86400)::bigint = FLOOR(tx.completed_date / 86400)::bigint)  -- SAME DAY (UTC)
         OR (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
        )
),

/* --------- MATCH FLAGS --------- */
scored AS (
    SELECT
        c.*,
        (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
        (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
        (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
        (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) 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 FLOOR(c.document_unix / 86400)::bigint = FLOOR(c.completed_date / 86400)::bigint) AS date_match  -- SAME DAY (UTC)
    FROM candidates c
),

/* --------- CONFIDENCE (fixed denominator = total weight) --------- */
conf AS (
    SELECT
        s.transaction_id,
        s.document_id,
        s.company_id_txt,
        s.company_id_uuid,
        w.weightset_id,
        w.total_weight,
        /* expose weights so we can store both weights and per-field scores */
        w.date           AS date_weight,
        w.invoice_number AS invoice_weight,
        w.order_number   AS order_weight,
        w.merchant_id    AS merchant_weight,
        w.amount_total   AS amount_weight,
        w.currency       AS currency_weight,

        /* flags */
        (s.date_match)     AS date_match,
        (s.invoice_match)  AS invoice_match,
        (s.order_match)    AS order_match,
        (s.merchant_match) AS merchant_match,
        (s.amount_match)   AS amount_match,
        (s.currency_match) AS currency_match,

        /* per-field scores (weight if matched else 0) */
        (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) AS date_score,
        (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) AS invoice_score,
        (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) AS order_score,
        (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) AS merchant_score,
        (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) AS amount_score,
        (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END) AS currency_score,

        /* numerator & confidence */
        (
          (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
          (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
          (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
          (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
          (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
          (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
        ) AS numerator,
        CASE WHEN w.total_weight > 0
             THEN ROUND(100.0 * (
                    (CASE WHEN s.date_match     THEN w.date           ELSE 0 END) +
                    (CASE WHEN s.invoice_match  THEN w.invoice_number ELSE 0 END) +
                    (CASE WHEN s.order_match    THEN w.order_number   ELSE 0 END) +
                    (CASE WHEN s.merchant_match THEN w.merchant_id    ELSE 0 END) +
                    (CASE WHEN s.amount_match   THEN w.amount_total   ELSE 0 END) +
                    (CASE WHEN s.currency_match THEN w.currency       ELSE 0 END)
                  ) / w.total_weight, 2)
             ELSE 0 END AS confidence,

        ABS(COALESCE((s.document_unix - s.completed_date) / 86400.0, 1000000))::int AS date_diff_days
    FROM scored s
    CROSS JOIN weights w
),

/* --------- PICK BEST DOC PER TX --------- */
best AS (
    SELECT DISTINCT ON (company_id_txt, transaction_id)
        c.company_id_txt,
        c.company_id_uuid,
        c.transaction_id,
        c.document_id,
        c.weightset_id,
        c.confidence
    FROM conf c
    WHERE c.confidence >= $4
    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
),

/* --------- INSERT reconciliations (dedupe via NOT EXISTS) --------- */
ins AS (
    INSERT INTO public.reconciliations (
        athena_view, transaction_id, document_id, company_id, confidence, weightset_id
    )
    SELECT
        $2::text,
        b.transaction_id::uuid,
        b.document_id::uuid,
        b.company_id_uuid,
        b.confidence,
        b.weightset_id
    FROM best b
    WHERE NOT EXISTS (
        SELECT 1
        FROM public.reconciliations r
        WHERE r.company_id     = b.company_id_uuid
          AND r.transaction_id = b.transaction_id
          AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* --------- INSERT per-field scores linked by reconciliation_id --------- */
ms AS (
    SELECT
        i.reconciliation_id,
        c.weightset_id,
        c.confidence,
        c.numerator,
        c.total_weight,
        /* flags */
        c.date_match, c.invoice_match, c.order_match, c.merchant_match, c.amount_match, c.currency_match,
        /* weights */
        c.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight,
        /* scores */
        c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_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
),
ins_ms AS (
    INSERT INTO public.reconciliations_match_scores (
        reconciliation_id,
        weightset_id,
        confidence,
        numerator,
        total_weight,
        date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
        date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
        date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
    )
    SELECT
        reconciliation_id,
        weightset_id,
        confidence,
        numerator,
        total_weight,
        date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
        date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
        date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
    FROM ms
    ON CONFLICT (reconciliation_id) DO UPDATE
      SET weightset_id   = EXCLUDED.weightset_id,
          confidence     = EXCLUDED.confidence,
          numerator      = EXCLUDED.numerator,
          total_weight   = EXCLUDED.total_weight,
          date_match     = EXCLUDED.date_match,
          invoice_match  = EXCLUDED.invoice_match,
          order_match    = EXCLUDED.order_match,
          merchant_match = EXCLUDED.merchant_match,
          amount_match   = EXCLUDED.amount_match,
          currency_match = EXCLUDED.currency_match,
          date_weight    = EXCLUDED.date_weight,
          invoice_weight = EXCLUDED.invoice_weight,
          order_weight   = EXCLUDED.order_weight,
          merchant_weight= EXCLUDED.merchant_weight,
          amount_weight  = EXCLUDED.amount_weight,
          currency_weight= EXCLUDED.currency_weight,
          date_score     = EXCLUDED.date_score,
          invoice_score  = EXCLUDED.invoice_score,
          order_score    = EXCLUDED.order_score,
          merchant_score = EXCLUDED.merchant_score,
          amount_score   = EXCLUDED.amount_score,
          currency_score = EXCLUDED.currency_score
    RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3 (ignored by same-day logic; kept for signature)
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v5(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v5'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH
fallback_w AS (
  SELECT
    date           AS fw_date,
    invoice_number AS fw_invoice,
    order_number   AS fw_order,
    merchant_id    AS fw_merchant,
    amount_total   AS fw_amount,
    currency       AS fw_currency
  FROM public.reconciliations_confidence_weights
  ORDER BY weightset_id
  LIMIT 1
),

reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),

/* FIXED: wrap fw.fw_* in MAX() so everything is aggregated */
reg_base AS (
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), MAX(fw.fw_date))     AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), MAX(fw.fw_invoice))  AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), MAX(fw.fw_order))    AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), MAX(fw.fw_merchant)) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), MAX(fw.fw_amount))   AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), MAX(fw.fw_currency)) AS base_currency
  FROM reg_base_raw b
  CROSS JOIN fallback_w fw
),
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
  FROM reg_company_raw
  GROUP BY company_id
),
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
  FROM reg_merchant_raw
  GROUP BY company_id, merchant_id
),

/* -------- source normalization -------- */
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,
    UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
    t.merchant_id::text AS merchant_id_txt
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
  SELECT
    d.document_id::uuid AS document_id,
    d.company_id::uuid  AS company_id_uuid,
    CASE
      WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
      WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
      WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
      WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
        THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
           AND split_part(btrim(d.document_date), '/', 1)::int > 12
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
      ELSE NULL
    END::bigint AS document_unix,
    UPPER(NULLIF(d.currency, '')) AS currency_norm,
    ROUND(d.total::numeric, 2)    AS total_num,
    UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,
    d.merchant_id::text AS merchant_id_txt
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* -------- candidates: company must match + at least one field match -------- */
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm  AS tx_order,     docs.order_id_norm   AS doc_order,
    tx.merchant_id_txt AS tx_merchant, docs.merchant_id_txt AS doc_merchant
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR
        (tx.merchant_id_txt IS NOT NULL AND docs.merchant_id_txt IS NOT NULL AND tx.merchant_id_txt = docs.merchant_id_txt) OR
        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $3) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

/* -------- resolve weights per candidate: merchant → company → base -------- */
resolved_weights AS (
  SELECT
    c.transaction_id, c.document_id, c.company_id_uuid,
    COALESCE(c.doc_merchant, c.tx_merchant) 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
  FROM candidates 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, c.tx_merchant)
),

/* -------- flags -------- */
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
    (c.doc_merchant IS NOT NULL AND c.tx_merchant IS NOT NULL AND c.doc_merchant = c.tx_merchant) AS merchant_match,
    (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) 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 ) <= $3) AS date_match
  FROM candidates c
),

/* -------- compute numerator / denominator / confidence -------- */
conf AS (
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight) AS total_weight,

    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_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.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)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_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)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight), 2)
         ELSE 0 END AS confidence,

    ABS( (s.document_unix - s.completed_date) / 86400.0 ) AS date_diff_days
  FROM scored s
  JOIN resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

/* -------- pick best doc per tx -------- */
best 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
  FROM conf c
  WHERE c.confidence >= $4
  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
),

/* -------- insert reconciliations -------- */
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id
  )
  SELECT
    $2::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, $1::uuid
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* -------- write per-field scores -------- */
ms AS (
  SELECT
    i.reconciliation_id,
    $1::uuid AS 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.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_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
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  )
  SELECT
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function reconciliations_weight_registry_upsert(p_weight_key text, p_weight_value numeric, p_company_id uuid DEFAULT NULL::uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row public.reconciliations_weight_registry;
  v_merchant text;
BEGIN
  v_merchant := NULLIF(btrim(p_merchant_id), '');

  IF p_weight_key NOT IN (
       'date','invoice_number','order_number','merchant_id','amount_total','currency','customer_name'
     ) THEN
    RAISE EXCEPTION
      'weight_key must be one of (date, invoice_number, order_number, merchant_id, amount_total, currency, customer_name). Got: %',
      p_weight_key USING ERRCODE = '22023';
  END IF;

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

  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_merchant, p_weight_key, p_weight_value, p_active)
  ON CONFLICT (company_id, merchant_id, weight_key)
  DO UPDATE SET weight_value = EXCLUDED.weight_value, active = EXCLUDED.active
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v6(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v6'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH
fallback_w AS (
  SELECT
    date           AS fw_date,
    invoice_number AS fw_invoice,
    order_number   AS fw_order,
    merchant_id    AS fw_merchant,
    amount_total   AS fw_amount,
    currency       AS fw_currency
  FROM public.reconciliations_confidence_weights
  ORDER BY weightset_id
  LIMIT 1
),

reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),

reg_base AS (
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), MAX(fw.fw_date))     AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), MAX(fw.fw_invoice))  AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), MAX(fw.fw_order))    AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), MAX(fw.fw_merchant)) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), MAX(fw.fw_amount))   AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), MAX(fw.fw_currency)) AS base_currency
  FROM reg_base_raw b
  CROSS JOIN fallback_w fw
),
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
  FROM reg_company_raw
  GROUP BY company_id
),
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
  FROM reg_merchant_raw
  GROUP BY company_id, merchant_id
),

/* ---------- normalization (uses transactions.name_beneficiary) ---------- */
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,
    UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    t.merchant_id::text AS tx_merchant_raw,

    /* beneficiary name normalized for fuzzy merchant match */
    UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS benef_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                         AS benef_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                               AS benef_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
  SELECT
    d.document_id::uuid AS document_id,
    d.company_id::uuid  AS company_id_uuid,

    CASE
      WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
      WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
      WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
      WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
        THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
           AND split_part(btrim(d.document_date), '/', 1)::int > 12
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'DD/MM/YYYY'))
      WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
        THEN EXTRACT(EPOCH FROM to_timestamp(btrim(d.document_date), 'MM/DD/YYYY'))
      ELSE NULL
    END::bigint AS document_unix,

    UPPER(NULLIF(d.currency, '')) AS currency_norm,
    ROUND(d.total::numeric, 2)    AS total_num,
    UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    /* use merchant_name (not merchant_id) for merchant matching */
    d.merchant_name::text AS doc_merchant_raw,

    UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS merch_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                      AS merch_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                            AS merch_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* candidates: company must match + any one field matches */
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm  AS tx_order,     docs.order_id_norm   AS doc_order,

    tx.tx_merchant_raw,
    docs.doc_merchant_raw,

    tx.benef_core,  tx.benef_core_ns,
    docs.merch_core, docs.merch_core_ns,

    tx.fiscal_year, tx.quarter, tx.month_text
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR

        /* fuzzy merchant: Discord Inc ↔ DISCORD, contains either way */
        (
          docs.merch_core_ns IS NOT NULL AND (
               (tx.benef_core_ns IS NOT NULL AND (tx.benef_core_ns = docs.merch_core_ns
                                               OR POSITION(docs.merch_core_ns IN tx.benef_core_ns) > 0
                                               OR POSITION(tx.benef_core_ns  IN docs.merch_core_ns) > 0))
            OR (tx.tx_merchant_raw IS NOT NULL AND
                POSITION(docs.merch_core IN UPPER(regexp_replace(tx.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
          )
        ) OR

        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $3) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

/* resolve weights for this scope */
resolved_weights 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
  FROM candidates 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)
),

/* flags (reuse fuzzy merchant logic for the boolean) */
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,

    (
      c.merch_core_ns IS NOT NULL AND (
           (c.benef_core_ns IS NOT NULL 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_merchant_raw IS NOT NULL AND
            POSITION(c.merch_core IN UPPER(regexp_replace(c.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
      )
    ) AS merchant_match,

    (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) 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 ) <= $3)                                          AS date_match
  FROM candidates c
),

/* compute confidence */
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.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight) AS total_weight,

    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_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.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)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_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)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight), 2)
         ELSE 0 END AS confidence,

    ABS( (s.document_unix - s.completed_date) / 86400.0 ) AS date_diff_days
  FROM scored s
  JOIN resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

/* pick best doc per tx */
best 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 >= $4
  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
),

/* insert reconciliations (+ fiscal fields) */
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
    fiscal_year, quarter, month
  )
  SELECT
    $2::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, $1::uuid,
    b.fiscal_year, b.quarter, b.month_text
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* write per-field breakdown */
ms AS (
  SELECT
    i.reconciliation_id,
    $1::uuid AS 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.date_weight, c.invoice_weight, c.order_weight, c.merchant_weight, c.amount_weight, c.currency_weight,
    c.date_score, c.invoice_score, c.order_score, c.merchant_score, c.amount_score, c.currency_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
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  )
  SELECT
    reconciliation_id,
    weightset_id,
    confidence,
    numerator,
    total_weight,
    date_match, invoice_match, order_match, merchant_match, amount_match, currency_match,
    date_weight, invoice_weight, order_weight, merchant_weight, amount_weight, currency_weight,
    date_score, invoice_score, order_score, merchant_score, amount_score, currency_score
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,     -- $1
            p_athena_view,      -- $2
            p_date_window_days, -- $3
            p_min_confidence    -- $4
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v7(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v7'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  TEXT;
    v_rows INTEGER := 0;
    v_status TEXT := 'ok';
    v_msg TEXT := 'success';
    v_start TIMESTAMPTZ := clock_timestamp();
    v_end   TIMESTAMPTZ;
    v_ms    BIGINT;
BEGIN
    v_sql := $f$
WITH
fallback_w AS (
  SELECT
    date           AS fw_date,
    invoice_number AS fw_invoice,
    order_number   AS fw_order,
    merchant_id    AS fw_merchant,
    amount_total   AS fw_amount,
    currency       AS fw_currency
  FROM public.reconciliations_confidence_weights
  ORDER BY weightset_id
  LIMIT 1
),

reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),

/* base/company/merchant weights, incl. customer_name (default 0 if absent) */
reg_base AS (
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), MAX(fw.fw_date))     AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), MAX(fw.fw_invoice))  AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), MAX(fw.fw_order))    AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), MAX(fw.fw_merchant)) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), MAX(fw.fw_amount))   AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), MAX(fw.fw_currency)) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0)                   AS base_customer
  FROM reg_base_raw b
  CROSS JOIN fallback_w fw
),
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 reg_company_raw
  GROUP BY company_id
),
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 reg_merchant_raw
  GROUP BY company_id, merchant_id
),

/* ---------- normalization ---------- */
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,
    UPPER(NULLIF(regexp_replace(t.invoice_nr::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(t.order_id::text,   '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    t.merchant_id::text AS tx_merchant_raw,

    /* beneficiary for merchant fuzzy (kept) */
    UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS benef_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                         AS benef_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(t.name_beneficiary::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                               AS benef_core_ns,

    /* NEW: consumer name for customer_name signal */
    UPPER(NULLIF(regexp_replace(t.name_consumer::text, '[^A-Z0-9 ]', '', 'g'), ''))                           AS consumer_upper,
    REPLACE(
      btrim(regexp_replace(UPPER(NULLIF(regexp_replace(t.name_consumer::text, '[^A-Z0-9 ]', '', 'g'), '')), '\s+', ' ', 'g')),
      ' ', ''
    )                                                                                                         AS consumer_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
),
docs AS (
  SELECT
    d.document_id::uuid AS document_id,
    d.company_id::uuid  AS company_id_uuid,

    /* Robust date → unix seconds:
       1) use document_date_unix if present
       2) support 13/10 digit epochs
       3) support ISO YYYY-MM-DD...
       4) support DD/MM/YYYY and MM/DD/YYYY
       5) support YYYY-Mon-DD and YYYY-Month-DD (e.g. 2025-Aug-31, 2025-August-31)
    */
    COALESCE(
      NULLIF(d.document_date_unix, 0)::bigint,
      CASE
        WHEN NULLIF(btrim(d.document_date), '') IS NULL THEN NULL
        WHEN btrim(d.document_date) ~ '^\d{13}$' THEN (btrim(d.document_date)::bigint / 1000)
        WHEN btrim(d.document_date) ~ '^\d{10}$' THEN btrim(d.document_date)::bigint
        WHEN btrim(d.document_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$'
          THEN EXTRACT(EPOCH FROM (btrim(d.document_date)::timestamptz))
        WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
             AND split_part(btrim(d.document_date), '/', 1)::int > 12
          THEN EXTRACT(EPOCH FROM (to_timestamp(btrim(d.document_date), 'DD/MM/YYYY') AT TIME ZONE 'UTC'))
        WHEN btrim(d.document_date) ~ '^\d{2}/\d{2}/\d{4}$'
          THEN EXTRACT(EPOCH FROM (to_timestamp(btrim(d.document_date), 'MM/DD/YYYY') AT TIME ZONE 'UTC'))
        /* NEW: YYYY-Mon-DD, e.g. 2025-Aug-31 */
        WHEN btrim(d.document_date) ~ '^\d{4}-[A-Za-z]{3}-\d{1,2}$'
          THEN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(d.document_date), '-', ' '), 'YYYY Mon DD') AT TIME ZONE 'UTC'))
        /* NEW: YYYY-Month-DD, e.g. 2025-August-31 */
        WHEN btrim(d.document_date) ~ '^\d{4}-[A-Za-z]+-\d{1,2}$'
          THEN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(d.document_date), '-', ' '), 'YYYY Month DD') AT TIME ZONE 'UTC'))
        ELSE NULL
      END
    )::bigint AS document_unix,

    UPPER(NULLIF(d.currency, '')) AS currency_norm,
    ROUND(d.total::numeric, 2)    AS total_num,
    UPPER(NULLIF(regexp_replace(d.invoice_number::text, '[^A-Z0-9]', '', 'g'), '')) AS invoice_nr_norm,
    UPPER(NULLIF(regexp_replace(d.order_id::text,       '[^A-Z0-9]', '', 'g'), '')) AS order_id_norm,

    /* merchant_name normalized (for merchant signal) */
    d.merchant_name::text AS doc_merchant_raw,
    UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS merch_upper,
    regexp_replace(
      UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    )                                                                                                      AS merch_core,
    REPLACE(regexp_replace(
      btrim(regexp_replace(
        UPPER(NULLIF(regexp_replace(d.merchant_name::text, '[^A-Z0-9 ]', '', 'g'), '')),
        '\s+', ' ', 'g'
      )),
      '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M', '', 'g'
    ), ' ', '')                                                                                            AS merch_core_ns,

    /* customer_info normalized to plain text for containment search */
    UPPER(NULLIF(regexp_replace(d.customer_info::text, '[^A-Z0-9 ]', '', 'g'), ''))                        AS custinfo_upper,
    REPLACE(
      btrim(regexp_replace(UPPER(NULLIF(regexp_replace(d.customer_info::text, '[^A-Z0-9 ]', '', 'g'), '')), '\s+', ' ', 'g')),
      ' ', ''
    )                                                                                                      AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
),

/* candidates: company must match + any one field matches */
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm  AS tx_order,     docs.order_id_norm   AS doc_order,

    tx.tx_merchant_raw,
    docs.doc_merchant_raw,
    tx.benef_core,  tx.benef_core_ns,
    docs.merch_core, docs.merch_core_ns,

    /* consumer/customer normalized fields */
    tx.consumer_core_ns,
    docs.custinfo_core_ns,

    tx.fiscal_year, tx.quarter, tx.month_text
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR

        /* fuzzy merchant: contains either way */
        (
          docs.merch_core_ns IS NOT NULL AND (
               (tx.benef_core_ns IS NOT NULL AND (tx.benef_core_ns = docs.merch_core_ns
                                               OR POSITION(docs.merch_core_ns IN tx.benef_core_ns) > 0
                                               OR POSITION(tx.benef_core_ns  IN docs.merch_core_ns) > 0))
            OR (tx.tx_merchant_raw IS NOT NULL AND
                POSITION(docs.merch_core IN UPPER(regexp_replace(tx.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
          )
        ) OR

        /* customer_name signal — consumer within customer_info (::text normalized) */
        (
          tx.consumer_core_ns IS NOT NULL AND docs.custinfo_core_ns IS NOT NULL AND
          ( tx.consumer_core_ns = docs.custinfo_core_ns
            OR POSITION(tx.consumer_core_ns IN docs.custinfo_core_ns) > 0
            OR POSITION(docs.custinfo_core_ns IN tx.consumer_core_ns) > 0 )
        ) OR

        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $3) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

/* resolve weights for this scope (incl. customer_name) */
resolved_weights 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 candidates 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)
),

/* flags (add customer_name) */
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,

    (
      c.merch_core_ns IS NOT NULL AND (
           (c.benef_core_ns IS NOT NULL 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_merchant_raw IS NOT NULL AND
            POSITION(c.merch_core IN UPPER(regexp_replace(c.tx_merchant_raw, '[^A-Z0-9 ]','', 'g'))) > 0)
      )
    ) AS merchant_match,

    /* customer_name boolean */
    (
      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 )
    ) AS customer_match,

    (c.debited_num  IS NOT NULL AND c.total_num   IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) 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 ) <= $3)                                          AS date_match
  FROM candidates c
),

/* compute confidence (include customer_name) */
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 resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

/* pick best doc per tx */
best 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 >= $4
  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
),

/* insert reconciliations (+ fiscal fields) */
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
    fiscal_year, quarter, month
  )
  SELECT
    $2::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, $1::uuid,
    b.fiscal_year, b.quarter, b.month_text
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

/* write per-field breakdown (adds customer_* columns) */
ms AS (
  SELECT
    i.reconciliation_id,
    $1::uuid AS 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
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    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
    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
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

    BEGIN
        EXECUTE v_sql
        USING
            p_weightset_id,
            p_athena_view,
            p_date_window_days,
            p_min_confidence
        INTO v_rows;
    EXCEPTION WHEN OTHERS THEN
        v_status := 'error';
        v_msg := SQLERRM;
    END;

    v_end := clock_timestamp();
    v_ms := FLOOR(EXTRACT(EPOCH FROM (v_end - v_start)) * 1000)::BIGINT;

    RETURN jsonb_build_object(
        'reconciliations_processed', v_rows,
        'status', v_status,
        'message', v_msg,
        'processing_time', v_ms
    );
END;
$$;

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

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

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

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

create or replace function norm_core_ns(p text) returns text
    immutable
    strict
    parallel safe
    language sql
as
$$
  SELECT REPLACE(
           regexp_replace(
             btrim(
               regexp_replace(
                 regexp_replace(upper(p), '[^A-Z0-9 ]', '', 'g'),
                 '\s+', ' ', 'g'
               )
             ),
             '\m(INC|INCORPORATED|LTD|LIMITED|LLC|CO|COMPANY|CORP|CORPORATION|BV|GMBH|SA|PLC)\M',
             '',
             'g'
           ),
           ' ',
           ''
         )
$$;

alter function norm_core_ns(unknown) owner to postgres;

grant execute on function norm_core_ns(unknown) to anon;

grant execute on function norm_core_ns(unknown) to authenticated;

grant execute on function norm_core_ns(unknown) to service_role;

create or replace function norm_code(p text) returns text
    immutable
    strict
    parallel safe
    language sql
as
$$
  SELECT NULLIF(regexp_replace(upper(p), '[^A-Z0-9]', '', 'g'), '')
$$;

alter function norm_code(unknown) owner to postgres;

grant execute on function norm_code(unknown) to anon;

grant execute on function norm_code(unknown) to authenticated;

grant execute on function norm_code(unknown) to service_role;

create or replace function norm_text_ns(p text) returns text
    immutable
    strict
    parallel safe
    language sql
as
$$
  SELECT REPLACE(
           btrim(
             regexp_replace(
               regexp_replace(upper(p), '[^A-Z0-9 ]', '', 'g'),
               '\s+', ' ', 'g'
             )
           ),
           ' ',
           ''
         )
$$;

alter function norm_text_ns(unknown) owner to postgres;

grant execute on function norm_text_ns(unknown) to anon;

grant execute on function norm_text_ns(unknown) to authenticated;

grant execute on function norm_text_ns(unknown) to service_role;

create or replace function parse_doc_date(p_date text, p_unix bigint) returns bigint
    stable
    language plpgsql
as
$$
BEGIN
  IF p_unix IS NOT NULL AND p_unix <> 0 THEN RETURN p_unix; END IF;
  IF p_date IS NULL OR btrim(p_date) = '' THEN RETURN NULL; END IF;

  IF btrim(p_date) ~ '^\d{13}$' THEN
    RETURN (btrim(p_date))::bigint / 1000;
  ELSIF btrim(p_date) ~ '^\d{10}$' THEN
    RETURN (btrim(p_date))::bigint;
  ELSIF btrim(p_date) ~ '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?([+-]\d{2}(:?\d{2})?|Z)?)?$' THEN
    RETURN EXTRACT(EPOCH FROM (btrim(p_date)::timestamptz))::bigint;
  ELSIF btrim(p_date) ~ '^\d{4}-[A-Za-z]{3}-\d{1,2}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'YYYY Mon DD') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{4}-[A-Za-z]+-\d{1,2}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'YYYY Month DD') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{1,2}-[A-Za-z]{3}-\d{4}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'DD Mon YYYY') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{1,2}-[A-Za-z]+-\d{4}$' THEN
    RETURN EXTRACT(EPOCH FROM (to_timestamp(replace(btrim(p_date), '-', ' '), 'DD Month YYYY') AT TIME ZONE 'UTC'))::bigint;
  ELSIF btrim(p_date) ~ '^\d{2}/\d{2}/\d{4}$' THEN
    IF split_part(btrim(p_date), '/', 1)::int > 12 THEN
      RETURN EXTRACT(EPOCH FROM (to_timestamp(btrim(p_date), 'DD/MM/YYYY') AT TIME ZONE 'UTC'))::bigint;
    ELSE
      RETURN EXTRACT(EPOCH FROM (to_timestamp(btrim(p_date), 'MM/DD/YYYY') AT TIME ZONE 'UTC'))::bigint;
    END IF;
  END IF;

  RETURN NULL;
END;
$$;

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

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

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

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

create or replace function f_import_customers_from_yuki(p_company_id uuid, p_payload jsonb)
    returns TABLE(domain text, customer_id uuid, action text)
    security definer
    SET search_path = public
    language plpgsql
as
$$
declare
  r jsonb;
  v_domain text;
  v_id uuid;
  v_created_at timestamp;
begin
  if not exists (select 1 from public.companies c where c.company_id = p_company_id) then
    raise exception 'Company % not found in public.companies', p_company_id;
  end if;

  if p_payload ? 'data' is false then
    raise exception 'Payload must contain a "data" array';
  end if;

  for r in select jsonb_array_elements(p_payload->'data') loop
    v_domain := nullif(r->>'Domain','');
    if v_domain is null then continue; end if;

    begin
      v_id := nullif(r->>'ID','')::uuid;
    exception when invalid_text_representation then
      v_id := null;
    end;

    if r ? 'Created' and r->>'Created' not in ('', 'null') then
      v_created_at := to_date(r->>'Created','DD-MM-YYYY')::timestamp;
    else
      v_created_at := null;
    end if;

    return query
    insert into public.customers as c
      ( customer_id, company_id, name, domain, created_at )
    values
      ( v_id, p_company_id, v_domain, v_domain, v_created_at )
    on conflict on constraint customers_pkey1  -- <<<<<< avoids "customer_id" ambiguity
    do update
      set company_id = excluded.company_id,
          name       = excluded.name,
          domain     = excluded.domain,
          created_at = coalesce(excluded.created_at, c.created_at)
    returning
      c.domain,
      c.customer_id,                                   -- explicit table column
      case when c.xmax = 0 then 'insert' else 'update' end as action;
  end loop;
end
$$;

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

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

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

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

create or replace function athena_reconcile_general_v8(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v8'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid) returns jsonb
    language plpgsql
as
$$
DECLARE
    v_sql  text;
    v_rows integer := 0;
    v_status text := 'ok';
    v_msg text := 'success';
    v_start timestamptz := clock_timestamp();
    v_end   timestamptz;
    v_ms    bigint;
BEGIN
v_sql := $f$
WITH
-- 1) Registry layers (base → company → merchant), registry-only
reg_base_raw AS (
  SELECT weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active
),
reg_company_raw AS (
  SELECT company_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE merchant_id IS NULL AND active
),
reg_merchant_raw AS (
  SELECT company_id, merchant_id, weight_key, weight_value
  FROM public.reconciliations_weight_registry
  WHERE active
),
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 reg_base_raw
),
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 reg_company_raw
  GROUP BY company_id
),
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 reg_merchant_raw
  GROUP BY company_id, merchant_id
),

-- 2) Normalize sources (helper fns)
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,
    public.norm_core_ns(t.name_beneficiary) AS benef_core_ns,

    public.norm_text_ns(t.name_consumer)    AS consumer_core_ns,

    t.fiscal_year,
    t.quarter,
    t.month::text                    AS month_text
  FROM public.transactions t
  WHERE COALESCE(t.reconciled, FALSE) = FALSE
    AND ($4 IS NULL OR t.company_id::uuid = $4)
),
docs AS (
  SELECT
    d.document_id::uuid             AS document_id,
    d.company_id::uuid              AS company_id_uuid,

    -- CAST numeric → bigint for parse_doc_date(p_date text, p_unix bigint)
    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,
    public.norm_core_ns(d.merchant_name)     AS merch_core_ns,

    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND ($4 IS NULL OR d.company_id = $4)
),

-- 3) Candidates: same company AND at least one signal
candidates AS (
  SELECT
    tx.transaction_id, docs.document_id,
    tx.company_id_txt, docs.company_id_uuid,
    tx.completed_date, docs.document_unix,
    tx.currency_norm_tx, docs.currency_norm,
    tx.debited_num, docs.total_num,
    tx.invoice_nr_norm AS tx_invoice,  docs.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,    docs.order_id_norm   AS doc_order,

    tx.tx_merchant_raw,  docs.doc_merchant_raw,
    tx.benef_core_ns,    docs.merch_core_ns,

    tx.consumer_core_ns, docs.custinfo_core_ns,

    tx.fiscal_year, tx.quarter, tx.month_text
  FROM tx
  JOIN docs
    ON tx.company_id_txt = docs.company_id_uuid::text
   AND (
        ROUND(docs.total_num, 2) = ROUND(tx.debited_num, 2) OR
        (tx.invoice_nr_norm IS NOT NULL AND docs.invoice_nr_norm IS NOT NULL AND tx.invoice_nr_norm = docs.invoice_nr_norm) OR
        (tx.order_id_norm  IS NOT NULL AND docs.order_id_norm  IS NOT NULL AND tx.order_id_norm  = docs.order_id_norm) OR

        -- fuzzy merchant contains either way
        (
          docs.merch_core_ns IS NOT NULL AND tx.benef_core_ns IS NOT NULL AND
          ( tx.benef_core_ns = docs.merch_core_ns
            OR POSITION(docs.merch_core_ns IN tx.benef_core_ns) > 0
            OR POSITION(tx.benef_core_ns  IN docs.merch_core_ns) > 0 )
        ) OR

        -- customer name signal: consumer within customer_info text
        (
          tx.consumer_core_ns IS NOT NULL AND docs.custinfo_core_ns IS NOT NULL AND
          ( tx.consumer_core_ns = docs.custinfo_core_ns
            OR POSITION(tx.consumer_core_ns IN docs.custinfo_core_ns) > 0
            OR POSITION(docs.custinfo_core_ns IN tx.consumer_core_ns) > 0 )
        ) OR

        (tx.completed_date IS NOT NULL AND docs.document_unix IS NOT NULL
           AND ABS( (docs.document_unix - tx.completed_date) / 86400.0 ) <= $2) OR
        (tx.currency_norm_tx IS NOT NULL AND docs.currency_norm IS NOT NULL AND tx.currency_norm_tx = docs.currency_norm)
      )
),

-- 4) Resolve weights (merchant → company → base)
resolved_weights 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 candidates 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)
),

-- 5) Flags
scored AS (
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,

    (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL 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 )) 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 )) AS customer_match,

    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND ROUND(c.total_num,2) = ROUND(c.debited_num,2)) 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 ) <= $2) AS date_match
  FROM candidates c
),

-- 6) Confidence
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 resolved_weights rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id
),

-- 7) Pick best doc per tx
best 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 >= $3
  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
),

-- 8) Insert reconciliations (weightset_id now NULL)
ins AS (
  INSERT INTO public.reconciliations (
    athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
    fiscal_year, quarter, month
  )
  SELECT
    $1::text,
    b.transaction_id, b.document_id, b.company_id_uuid,
    b.confidence, NULL::uuid,
    b.fiscal_year, b.quarter, b.month_text
  FROM best b
  WHERE NOT EXISTS (
    SELECT 1 FROM public.reconciliations r
    WHERE r.company_id = b.company_id_uuid
      AND r.transaction_id = b.transaction_id
      AND r.document_id = b.document_id
  )
  RETURNING reconciliation_id, company_id, transaction_id, document_id
),

-- 9) Insert breakdown
ms AS (
  SELECT
    i.reconciliation_id,
    NULL::uuid AS 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
),
ins_ms AS (
  INSERT INTO public.reconciliations_match_scores (
    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
    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
  FROM ms
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score
  RETURNING 1
)
SELECT count(*) FROM ins
$f$;

BEGIN
  EXECUTE v_sql
  USING
    p_athena_view,       -- $1
    p_date_window_days,  -- $2
    p_min_confidence,    -- $3
    p_company_id         -- $4
  INTO v_rows;
EXCEPTION WHEN OTHERS THEN
  v_status := 'error';
  v_msg := SQLERRM;
END;

v_end := clock_timestamp();
v_ms := floor(extract(epoch from (v_end - v_start)) * 1000)::bigint;

RETURN jsonb_build_object(
  'reconciliations_processed', v_rows,
  'status', v_status,
  'message', v_msg,
  'processing_time', v_ms
);
END;
$$;

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

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

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

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

create or replace function f_import_yuki_domains(p_payload jsonb)
    returns TABLE(domain text, yuki_domain_id uuid, action text)
    security definer
    SET search_path = public
    language plpgsql
as
$$
declare
  r jsonb;
  v_domain text;
begin
  if p_payload ? 'data' is false then
    raise exception 'Payload must contain a "data" array';
  end if;

  for r in select jsonb_array_elements(p_payload->'data') loop
    v_domain := r->>'Domain';
    if coalesce(v_domain,'') = '' then continue; end if;

    return query
    insert into public.yuki_domains as yd
      ( yuki_domain_id
      , domain
      , status_id
      , created
      , status
      , companies
      , workflow
      , questions
      , bobyparent
      , bundle
      , trial
      , signed_gdpr_document
      , allow_self_accounting
      , users
      , archived_on
      , inactive_on
      , is_overdue
      , portal
      )
    values
      (
        nullif(r->>'ID','')::uuid,
        v_domain,
        nullif(r->>'StatusId','')::int,
        case when r ? 'Created' and r->>'Created' not in ('', 'null') then to_date(r->>'Created','DD-MM-YYYY') end,
        nullif(r->>'Status',''),
        nullif(r->>'Companies','')::int,
        nullif(r->>'Workflow','')::int,
        nullif(r->>'Questions','')::int,
        coalesce(nullif(r->>'BOByParent','')::int, 0),
        nullif(r->>'Bundle',''),
        case
          when (r ? 'Trial') is false or r->>'Trial' = '' then null
          when lower(r->>'Trial') in ('true','t','yes','y','1') then true
          when lower(r->>'Trial') in ('false','f','no','n','0') then false
          else (nullif(r->>'Trial','')::int <> 0)
        end,
        (r->>'SignedGDPRDocument')::boolean,
        nullif(r->>'AllowSelfAccounting','')::int,
        nullif(r->>'Users','')::int,
        case when r ? 'ArchivedOn' and r->>'ArchivedOn' not in ('', 'null') then to_timestamp(r->>'ArchivedOn','DD-MM-YYYY')::timestamptz end,
        case when r ? 'InactiveOn' and r->>'InactiveOn' not in ('', 'null') then to_date(r->>'InactiveOn','DD-MM-YYYY') end,
        coalesce(
          nullif(r->>'IsOverdue','')::int,
          case when (r->>'IsOverdue')::boolean is true then 1
               when (r->>'IsOverdue')::boolean is false then 0 end
        ),
        nullif(r->>'Portal','')
      )
    on conflict (domain) do update
      set yuki_domain_id          = coalesce(excluded.yuki_domain_id, yd.yuki_domain_id),
          status_id               = excluded.status_id,
          created                 = excluded.created,
          status                  = excluded.status,
          companies               = excluded.companies,
          workflow                = excluded.workflow,
          questions               = excluded.questions,
          bobyparent              = excluded.bobyparent,
          bundle                  = excluded.bundle,
          trial                   = excluded.trial,
          signed_gdpr_document    = excluded.signed_gdpr_document,
          allow_self_accounting   = excluded.allow_self_accounting,
          users                   = excluded.users,
          archived_on             = excluded.archived_on,
          inactive_on             = excluded.inactive_on,
          is_overdue              = excluded.is_overdue,
          portal                  = excluded.portal
    returning
      yd.domain,
      yd.yuki_domain_id,
      case when yd.xmax = 0 then 'insert' else 'update' end;
  end loop;
end
$$;

alter function f_import_yuki_domains(unknown) owner to postgres;

grant execute on function f_import_yuki_domains(unknown) to anon;

grant execute on function f_import_yuki_domains(unknown) to authenticated;

grant execute on function f_import_yuki_domains(unknown) to service_role;

create or replace function athena_reconcile_general_v9_fast(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v9_fast'::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) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';

  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  ---------------------------------------------------------------------------
  -- 0) REGISTRY SNAPSHOTS (small tables, cheap)
  ---------------------------------------------------------------------------
  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
    GET DIAGNOSTICS _count = ROW_COUNT;
    t1 := clock_timestamp();
    RAISE NOTICE 'registry snapshotted (% ms)', EXTRACT(MILLISECOND FROM t1 - t0);
    j_timings := j_timings || jsonb_build_object('registry_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 1) NORMALIZE SOURCES INTO TEMP TABLES + LOCAL INDEXES
  ---------------------------------------------------------------------------
  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,
    public.norm_core_ns(t.name_beneficiary) AS benef_core_ns,
    public.norm_text_ns(t.name_consumer)    AS consumer_core_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);
  -- numeric indexes: debited_num frequently used
  CREATE INDEX ON _tx (debited_num);

  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,
    public.norm_core_ns(d.merchant_name)         AS merch_core_ns,

    public.norm_text_ns(d.customer_info::text)   AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

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

  IF p_debug THEN
    t2 := clock_timestamp();
    RAISE NOTICE '_tx/_docs built + indexed (% ms)', EXTRACT(MILLISECOND FROM t2 - t1);
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 2) CANDIDATES via UNION ALL branches (no giant OR) + DISTINCT
  ---------------------------------------------------------------------------
  -- Amount match
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  -- Invoice code match
  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  -- Order code match
  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  -- Merchant fuzzy
  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  -- Customer name signal
  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND d.custinfo_core_ns IS NOT NULL
   AND (
        tx.consumer_core_ns = d.custinfo_core_ns
     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0
   );

  -- Date window
  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  -- Currency equality
  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  -- Union all branches, dedupe once
  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_date
    UNION ALL
    SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

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

  ---------------------------------------------------------------------------
  -- 3) RESOLVE WEIGHTS (merchant → company → base)
  ---------------------------------------------------------------------------
  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);

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _rw;
    RAISE NOTICE 'resolved_weights: % rows (% ms)', _count, EXTRACT(MILLISECOND FROM t2 - t1);
    j_timings := j_timings
      || jsonb_build_object('resolved_rows', _count)
      || jsonb_build_object('resolved_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 4) FLAGS + CONFIDENCE (on temp tables)
  ---------------------------------------------------------------------------
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
    (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL 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 )) 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 )) 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);

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _conf;
    RAISE NOTICE 'flags+conf: % rows (% ms)', _count, EXTRACT(MILLISECOND FROM t1 - t2);
    j_timings := j_timings
      || jsonb_build_object('conf_rows', _count)
      || jsonb_build_object('conf_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- 5) BEST per tx + INSERTS
  ---------------------------------------------------------------------------
  CREATE TEMP TABLE _best 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;

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _best;
    RAISE NOTICE 'best: % rows (% ms)', _count, EXTRACT(MILLISECOND FROM t2 - t1);
    j_timings := j_timings
      || jsonb_build_object('best_rows', _count)
      || jsonb_build_object('best_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
  END IF;

  -- Insert reconciliations
  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, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    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
    i.reconciliation_id,
    NULL::uuid AS 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
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  IF p_debug THEN
    t1 := clock_timestamp();
    RAISE NOTICE 'inserted/updated ms rows: % (% ms)', v_rows, EXTRACT(MILLISECOND FROM t1 - t2);
    j_timings := j_timings
      || jsonb_build_object('inserted_rows', v_rows)
      || jsonb_build_object('insert_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
  END IF;

  ---------------------------------------------------------------------------
  -- Done
  ---------------------------------------------------------------------------
  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

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

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

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

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

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

create or replace function athena_reconcile_general_v10(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v10'::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) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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);
    RAISE NOTICE 'registry: % ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t0)))::bigint;
  END IF;

  /* 1) Normalize sources into temp tables + indexes */
  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,
    public.norm_core_ns(t.name_beneficiary) AS benef_core_ns,
    public.norm_text_ns(t.name_consumer)    AS consumer_core_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 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,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,
    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

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

  IF p_debug THEN
    t2 := clock_timestamp();
    j_timings := j_timings || jsonb_build_object('normalize_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
    RAISE NOTICE 'normalize+index: % ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint;
  END IF;

  /* 2) Candidates via targeted branches (UNION ALL) */
  CREATE TEMP TABLE _cand_amount AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  CREATE TEMP TABLE _cand_invoice AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  CREATE TEMP TABLE _cand_order AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  CREATE TEMP TABLE _cand_merchant AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_customer AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND d.custinfo_core_ns IS NOT NULL
   AND (
        tx.consumer_core_ns = d.custinfo_core_ns
     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0
   );

  CREATE TEMP TABLE _cand_date AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  CREATE TEMP TABLE _cand_currency AS
  SELECT DISTINCT ON (tx.transaction_id, d.document_id)
    tx.transaction_id, d.document_id, tx.company_id_txt, d.company_id_uuid,
    tx.completed_date, d.document_unix, tx.currency_norm_tx, d.currency_norm,
    tx.debited_num, d.total_num,
    tx.invoice_nr_norm AS tx_invoice, d.invoice_nr_norm AS doc_invoice,
    tx.order_id_norm   AS tx_order,   d.order_id_norm   AS doc_order,
    tx.tx_merchant_raw, d.doc_merchant_raw,
    tx.benef_core_ns, d.merch_core_ns,
    tx.consumer_core_ns, d.custinfo_core_ns,
    tx.fiscal_year, tx.quarter, tx.month_text
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

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

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

  /* 3) Resolve weights (merchant → company → base) */
  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);

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _rw;
    j_timings := j_timings
      || jsonb_build_object('resolved_rows', _count)
      || jsonb_build_object('resolved_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
    RAISE NOTICE 'resolved: % rows, % ms', _count, ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint;
  END IF;

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,
    (c.doc_invoice  IS NOT NULL AND c.tx_invoice  IS NOT NULL AND c.doc_invoice  = c.tx_invoice ) AS invoice_match,
    (c.doc_order    IS NOT NULL AND c.tx_order    IS NOT NULL AND c.doc_order    = c.tx_order   ) AS order_match,
    (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL 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 )) 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 )) 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);

  IF p_debug THEN
    t1 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _conf;
    j_timings := j_timings
      || jsonb_build_object('conf_rows', _count)
      || jsonb_build_object('conf_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
    RAISE NOTICE 'conf: % rows, % ms', _count, ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint;
  END IF;

  /* 5) Best per tx + inserts */
  CREATE TEMP TABLE _best 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;

  IF p_debug THEN
    t2 := clock_timestamp();
    SELECT COUNT(*) INTO _count FROM _best;
    j_timings := j_timings
      || jsonb_build_object('best_rows', _count)
      || jsonb_build_object('best_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint);
    RAISE NOTICE 'best: % rows, % ms', _count, ROUND(1000 * EXTRACT(EPOCH FROM (t2 - t1)))::bigint;
  END IF;

  WITH ins AS (
    INSERT INTO public.reconciliations (
      athena_view, transaction_id, document_id, company_id, confidence, weightset_id,
      fiscal_year, quarter, month
    )
    SELECT
      p_athena_view,
      b.transaction_id, b.document_id, b.company_id_uuid,
      b.confidence, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    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
    i.reconciliation_id,
    NULL::uuid AS 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
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  IF p_debug THEN
    t1 := clock_timestamp();
    j_timings := j_timings
      || jsonb_build_object('inserted_rows', v_rows)
      || jsonb_build_object('insert_ms', ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint);
    RAISE NOTICE 'insert: % rows, % ms', v_rows, ROUND(1000 * EXTRACT(EPOCH FROM (t1 - t2)))::bigint;
  END IF;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

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

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

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

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

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

create or replace function f_suggest_customer_domain(p_name text, p_company_id uuid DEFAULT NULL::uuid) returns text
    stable
    language sql
as
$$
with raw_name as (
  select coalesce(nullif(p_name, ''), 'customer') as n
),
base_slug as (
  select lower(
           regexp_replace(                       -- trim leading/trailing underscores
             regexp_replace(                     -- replace non-alnum with underscores
               unaccent(n),
               '[^a-zA-Z0-9]+', '_', 'g'
             ),
             '^_+|_+$', '', 'g'
           )
         ) as base
  from raw_name
),
norm as (
  select case when base = '' then 'customer' else base end as base from base_slug
),
candidates as (
  -- Try the base slug, then add suffixes _2, _3, ...
  select base as candidate, 0 as n from norm
  union all
  select base || '_' || gs.n::text as candidate, gs.n
  from norm, generate_series(2, 9999) as gs(n)
),
taken as (
  select lower(c.domain) as dkey
  from public.customers c
  where c.domain is not null
    and (p_company_id is null or c.company_id = p_company_id)
),
first_free as (
  select c.candidate
  from candidates c
  left join taken t on t.dkey = c.candidate
  where t.dkey is null
  order by c.n asc
  limit 1
)
select candidate from first_free;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v11(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v11'::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) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weights snapshot (registry only) */
  CREATE TEMP TABLE _reg_base AS
  SELECT
    COALESCE(MAX(CASE WHEN weight_key='date'           THEN weight_value END), 0) AS base_date,
    COALESCE(MAX(CASE WHEN weight_key='invoice_number' THEN weight_value END), 0) AS base_invoice,
    COALESCE(MAX(CASE WHEN weight_key='order_number'   THEN weight_value END), 0) AS base_order,
    COALESCE(MAX(CASE WHEN weight_key='merchant_id'    THEN weight_value END), 0) AS base_merchant,
    COALESCE(MAX(CASE WHEN weight_key='amount_total'   THEN weight_value END), 0) AS base_amount,
    COALESCE(MAX(CASE WHEN weight_key='currency'       THEN weight_value END), 0) AS base_currency,
    COALESCE(MAX(CASE WHEN weight_key='customer_name'  THEN weight_value END), 0) AS base_customer
  FROM public.reconciliations_weight_registry
  WHERE company_id IS NULL AND merchant_id IS NULL AND active;

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

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

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

  /* 1) Normalize sources → temp tables + indexes */
  CREATE TEMP TABLE _tx AS
  SELECT
    t.transaction_id::uuid          AS transaction_id,
    t.company_id::text              AS company_id_txt,
    COALESCE(t.completed_date, t.paid_at::bigint, t.started_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,
    public.norm_core_ns(t.name_beneficiary)                AS benef_core_ns,
    public.norm_core_ns(t.account_holder_name_counterparty) AS counterparty_core_ns,

    public.norm_code(t.description)            AS desc_code,
    public.norm_code(t.remittance_information) AS remit_code,
    public.norm_code(t.processed_description)  AS pdesc_code,

    public.norm_iban(t.iban_counterparty)      AS tx_iban_cp,
    public.norm_iban(t.iban_beneficiary)       AS tx_iban_ben,
    public.norm_code(t.bic_counterparty)       AS tx_bic_cp,
    public.norm_code(t.bic)                    AS tx_bic,

    public.norm_text_ns(t.name_consumer)       AS consumer_core_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 (counterparty_core_ns);
  CREATE INDEX ON _tx (consumer_core_ns);
  CREATE INDEX ON _tx (debited_num);
  CREATE INDEX ON _tx (tx_iban_cp);
  CREATE INDEX ON _tx (tx_iban_ben);
  CREATE INDEX ON _tx (tx_bic_cp);
  CREATE INDEX ON _tx (tx_bic);

  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,
    public.norm_code(d.receipt_number)         AS receipt_nr_norm,

    d.merchant_name::text                      AS doc_merchant_raw,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,

    public.norm_iban((d.merchant_info::jsonb ->> 'iban')) AS doc_iban,
    public.norm_code((d.merchant_info::jsonb ->> 'bic'))  AS doc_bic,

    (
      COALESCE( (d.totals_details::jsonb ->> 'total_paid')::numeric, d.total_paid::numeric, 0 )
      >=
      COALESCE( (d.totals_details::jsonb ->> 'total')::numeric, d.total::numeric, 0 )
    )
    OR
    COALESCE(d.amount_due::numeric, (d.totals_details::jsonb ->> 'amount_due')::numeric) = 0
    AS doc_paid,

    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

  CREATE INDEX ON _docs (company_id_uuid);
  CREATE INDEX ON _docs (document_unix);
  CREATE INDEX ON _docs (invoice_nr_norm);
  CREATE INDEX ON _docs (order_id_norm);
  CREATE INDEX ON _docs (receipt_nr_norm);
  CREATE INDEX ON _docs (currency_norm);
  CREATE INDEX ON _docs (merch_core_ns);
  CREATE INDEX ON _docs (doc_iban);
  CREATE INDEX ON _docs (doc_bic);
  CREATE INDEX ON _docs (custinfo_core_ns);
  CREATE INDEX ON _docs (total_num);

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

  /* 2) Candidates via UNION branches */
  -- Amount
  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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  -- Invoice exact
  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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL AND d.invoice_nr_norm = tx.invoice_nr_norm;

  -- Invoice via tx free-text tokens
  CREATE TEMP TABLE _cand_invoice_ref 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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.invoice_nr_norm IS NOT NULL
   AND (tx.desc_code  = d.invoice_nr_norm
     OR tx.remit_code = d.invoice_nr_norm
     OR tx.pdesc_code = d.invoice_nr_norm);

  -- Order exact (or receipt)
  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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND (
        (tx.order_id_norm IS NOT NULL AND d.order_id_norm   = tx.order_id_norm) OR
        (tx.order_id_norm IS NOT NULL AND d.receipt_nr_norm = tx.order_id_norm)
       );

  -- Merchant fuzzy (name)
  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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL AND d.merch_core_ns IS NOT NULL
   AND (tx.benef_core_ns = d.merch_core_ns
        OR position(d.merch_core_ns in tx.benef_core_ns) > 0
        OR position(tx.benef_core_ns in d.merch_core_ns) > 0);

  -- Bank identity (IBAN/BIC equality)
  CREATE TEMP TABLE _cand_bank 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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND (
        (tx.tx_iban_cp IS NOT NULL AND d.doc_iban IS NOT NULL AND tx.tx_iban_cp = d.doc_iban) OR
        (tx.tx_iban_ben IS NOT NULL AND d.doc_iban IS NOT NULL AND tx.tx_iban_ben = d.doc_iban) OR
        (tx.tx_bic_cp  IS NOT NULL AND d.doc_bic  IS NOT NULL AND tx.tx_bic_cp  = d.doc_bic) OR
        (tx.tx_bic     IS NOT NULL AND d.doc_bic  IS NOT NULL AND tx.tx_bic     = d.doc_bic)
       );

  -- Customer name
  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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL AND d.custinfo_core_ns IS NOT NULL
   AND (tx.consumer_core_ns = d.custinfo_core_ns
        OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
        OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0);

  -- Date window
  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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  -- Currency equality
  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.receipt_nr_norm AS doc_receipt,
    d.doc_merchant_raw, d.merch_core_ns, d.custinfo_core_ns, d.doc_iban, d.doc_bic, d.doc_paid
  FROM _tx tx JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

  -- Union + dedupe
  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_invoice_ref
    UNION ALL SELECT * FROM _cand_order
    UNION ALL SELECT * FROM _cand_merchant
    UNION ALL SELECT * FROM _cand_bank
    UNION ALL SELECT * FROM _cand_customer
    UNION ALL SELECT * FROM _cand_date
    UNION ALL SELECT * FROM _cand_currency
  ) u
  ORDER BY transaction_id, document_id;

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

  /* 3) Resolve weights (merchant → company → base) */
  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);

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

  /* 4) Flags + confidence */
  CREATE TEMP TABLE _scored AS
  SELECT
    c.*,

    -- invoice (exact or via ref tokens)
    (c.doc_invoice IS NOT NULL AND c.invoice_nr_norm IS NOT NULL AND c.doc_invoice = c.invoice_nr_norm)
    OR (c.doc_invoice IS NOT NULL AND (c.desc_code = c.doc_invoice OR c.remit_code = c.doc_invoice OR c.pdesc_code = c.doc_invoice))
    AS invoice_match,

    -- order/receipt
    (c.doc_order IS NOT NULL AND c.order_id_norm IS NOT NULL AND c.doc_order = c.order_id_norm)
    OR (c.doc_receipt IS NOT NULL AND c.order_id_norm IS NOT NULL AND c.doc_receipt = c.order_id_norm)
    AS order_match,

    -- merchant: fuzzy OR bank identity OR counterparty-name fuzzy
    (
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL 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.counterparty_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL AND
        ( position(c.merch_core_ns in c.counterparty_core_ns) > 0
          OR position(c.counterparty_core_ns in c.merch_core_ns) > 0 ))
      OR
      (c.doc_iban IS NOT NULL AND (c.tx_iban_cp = c.doc_iban OR c.tx_iban_ben = c.doc_iban))
      OR
      (c.doc_bic  IS NOT NULL AND (c.tx_bic_cp  = c.doc_bic  OR c.tx_bic      = c.doc_bic))
    ) AS merchant_match,

    -- customer name
    (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 )) AS customer_match,

    -- amount/currency/date
    (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,
    s.doc_paid
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

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

  /* 5) Best per transaction (paid tiebreaker) + inserts */
  CREATE TEMP TABLE _best 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.doc_paid
  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,
           doc_paid DESC,
           document_id;

  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, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    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
    i.reconciliation_id,
    NULL::uuid AS 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
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

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

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

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

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

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

create or replace function athena_reconcile_general_v12(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v12'::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) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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;

  /* 1) Normalize sources into temp tables + indexes */
  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,

    -- FIX: merchant/beneficiary fallback
    public.norm_core_ns(
      COALESCE(t.name_beneficiary, t.account_holder_name_counterparty, t.bank_counterparty)
    ) AS benef_core_ns,

    public.norm_text_ns(t.name_consumer) AS consumer_core_ns,

    -- IBAN/BIC (normalized for matching and candidate generation)
    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_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 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,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,

    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns,

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

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

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

  /* 2) Candidates via targeted branches (UNION ALL) */
  -- amount
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

  -- invoice code
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm IS NOT NULL
   AND d.invoice_nr_norm = tx.invoice_nr_norm;

  -- order code
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.order_id_norm IS NOT NULL
   AND d.order_id_norm IS NOT NULL
   AND d.order_id_norm = tx.order_id_norm;

  -- merchant fuzzy
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.benef_core_ns IS NOT NULL
   AND d.merch_core_ns  IS NOT NULL
   AND (
        tx.benef_core_ns = d.merch_core_ns
     OR position(d.merch_core_ns in tx.benef_core_ns) > 0
     OR position(tx.benef_core_ns in d.merch_core_ns) > 0
   );

  -- customer name
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND tx.consumer_core_ns IS NOT NULL
   AND d.custinfo_core_ns IS NOT NULL
   AND (
        tx.consumer_core_ns = d.custinfo_core_ns
     OR position(tx.consumer_core_ns in d.custinfo_core_ns) > 0
     OR position(d.custinfo_core_ns in tx.consumer_core_ns) > 0
   );

  -- NEW: IBAN
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND length(tx.tx_iban_ns) >= 15
     AND position(tx.tx_iban_ns in d.info_ns) > 0;

  -- NEW: BIC
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  -- date window
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

  -- currency equality
  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.currency_norm_tx IS NOT NULL
     AND d.currency_norm    IS NOT NULL
     AND tx.currency_norm_tx = d.currency_norm;

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

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

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

  CREATE INDEX ON _rw (transaction_id, document_id);

  /* 4) Flags + confidence (IBAN/BIC contribute to merchant_match) */
  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,

    (
      -- merchant fuzzy name
      (c.benef_core_ns IS NOT NULL AND c.merch_core_ns IS NOT NULL 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: IBAN/BIC confirms merchant relation
      OR (c.tx_iban_ns IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_iban_ns in c.info_ns) > 0)
      OR (c.tx_bic_ns  IS NOT NULL AND c.info_ns IS NOT NULL AND position(c.tx_bic_ns  in c.info_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 )) AS customer_match,

    (c.debited_num IS NOT NULL AND c.total_num IS NOT NULL AND c.total_num = c.debited_num) AS amount_match,
    (c.currency_norm_tx IS NOT NULL AND c.currency_norm IS NOT NULL AND c.currency_norm_tx = c.currency_norm) AS currency_match,
    (c.document_unix IS NOT NULL AND c.completed_date IS NOT NULL
       AND abs((c.document_unix - c.completed_date)/86400.0) <= p_date_window_days) AS date_match
  FROM _cand c;

  CREATE TEMP TABLE _conf AS
  SELECT
    s.transaction_id, s.document_id, s.company_id_txt, s.company_id_uuid,
    s.fiscal_year, s.quarter, s.month_text,
    rw.date_weight, rw.invoice_weight, rw.order_weight, rw.merchant_weight, rw.amount_weight, rw.currency_weight, rw.customer_weight,
    (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) AS total_weight,
    s.date_match, s.invoice_match, s.order_match, s.merchant_match, s.amount_match, s.currency_match, s.customer_match,

    (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) AS date_score,
    (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) AS invoice_score,
    (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) AS order_score,
    (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) AS merchant_score,
    (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) AS amount_score,
    (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) AS currency_score,
    (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END) AS customer_score,

    (
      (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
      (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
      (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
      (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
      (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
      (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
      (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
    ) AS numerator,

    CASE WHEN (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight) > 0
         THEN ROUND(100.0 * (
              (CASE WHEN s.date_match     THEN rw.date_weight     ELSE 0 END) +
              (CASE WHEN s.invoice_match  THEN rw.invoice_weight  ELSE 0 END) +
              (CASE WHEN s.order_match    THEN rw.order_weight    ELSE 0 END) +
              (CASE WHEN s.merchant_match THEN rw.merchant_weight ELSE 0 END) +
              (CASE WHEN s.amount_match   THEN rw.amount_weight   ELSE 0 END) +
              (CASE WHEN s.currency_match THEN rw.currency_weight ELSE 0 END) +
              (CASE WHEN s.customer_match THEN rw.customer_weight ELSE 0 END)
           ) / (rw.date_weight + rw.invoice_weight + rw.order_weight + rw.merchant_weight + rw.amount_weight + rw.currency_weight + rw.customer_weight), 2)
         ELSE 0 END AS confidence,

    abs((s.document_unix - s.completed_date)/86400.0) AS date_diff_days
  FROM _scored s
  JOIN _rw rw
    ON rw.transaction_id = s.transaction_id
   AND rw.document_id    = s.document_id;

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

  /* 5) Best per tx + inserts */
  CREATE TEMP TABLE _best 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;

  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, NULL::uuid,
      b.fiscal_year, b.quarter, b.month_text
    FROM _best b
    WHERE NOT EXISTS (
      SELECT 1 FROM public.reconciliations r
       WHERE r.company_id     = b.company_id_uuid
         AND r.transaction_id = b.transaction_id
         AND r.document_id    = b.document_id
    )
    RETURNING reconciliation_id, company_id, transaction_id, document_id
  )
  INSERT INTO public.reconciliations_match_scores (
    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
    i.reconciliation_id,
    NULL::uuid AS 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
  ON CONFLICT (reconciliation_id) DO UPDATE
    SET weightset_id     = EXCLUDED.weightset_id,
        confidence       = EXCLUDED.confidence,
        numerator        = EXCLUDED.numerator,
        total_weight     = EXCLUDED.total_weight,
        date_match       = EXCLUDED.date_match,
        invoice_match    = EXCLUDED.invoice_match,
        order_match      = EXCLUDED.order_match,
        merchant_match   = EXCLUDED.merchant_match,
        amount_match     = EXCLUDED.amount_match,
        currency_match   = EXCLUDED.currency_match,
        customer_match   = EXCLUDED.customer_match,
        date_weight      = EXCLUDED.date_weight,
        invoice_weight   = EXCLUDED.invoice_weight,
        order_weight     = EXCLUDED.order_weight,
        merchant_weight  = EXCLUDED.merchant_weight,
        amount_weight    = EXCLUDED.amount_weight,
        currency_weight  = EXCLUDED.currency_weight,
        customer_weight  = EXCLUDED.customer_weight,
        date_score       = EXCLUDED.date_score,
        invoice_score    = EXCLUDED.invoice_score,
        order_score      = EXCLUDED.order_score,
        merchant_score   = EXCLUDED.merchant_score,
        amount_score     = EXCLUDED.amount_score,
        currency_score   = EXCLUDED.currency_score,
        customer_score   = EXCLUDED.customer_score;

  GET DIAGNOSTICS v_rows = ROW_COUNT;

  RETURN jsonb_build_object(
    'reconciliations_processed', v_rows,
    'status', v_status,
    'message', v_msg,
    'processing_time', ROUND(1000 * EXTRACT(EPOCH FROM (clock_timestamp() - t0)))::bigint,
    'timings', j_timings
  );

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

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

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

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

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

create or replace function reconciliations_staging_update_v1(p_reconciliation_id uuid, p_column text DEFAULT NULL::text, p_column_value boolean DEFAULT NULL::boolean, p_verified boolean DEFAULT NULL::boolean, p_company_id uuid DEFAULT NULL::uuid, p_organization_id uuid DEFAULT NULL::uuid, p_user_id uuid DEFAULT NULL::uuid, p_publish_event boolean DEFAULT true) returns jsonb
    language plpgsql
as
$$
DECLARE
  v_col text;
  v_company_id      uuid;
  v_document_id     uuid;
  v_transaction_id  uuid;

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

  v_stage_found     boolean := false;
  v_stage_updated   int := 0;
  v_stage_toggle_upd int := 0;
  v_recon_upd       int := 0;

  j_result jsonb := jsonb_build_object('ok', true);
BEGIN
  /* 0) Resolve the canonical reconciliation row (for link discovery & verified write) */
  SELECT r.company_id, r.document_id, r.transaction_id
    INTO v_company_id, v_document_id, v_transaction_id
  FROM public.reconciliations r
  WHERE r.reconciliation_id = p_reconciliation_id;

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

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

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

    IF p_column_value IS NULL THEN
      RETURN jsonb_build_object(
        'ok', false,
        'error', 'missing_value'
      );
    END IF;

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

    GET DIAGNOSTICS v_stage_toggle_upd = ROW_COUNT;

    IF v_stage_toggle_upd = 0 THEN
      -- Staging row not found; surface as internal to match your API’s behavior expectations
      RETURN jsonb_build_object('ok', false, 'error', 'internal', 'message', 'staging_row_not_found');
    END IF;

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

  /* 2) Optional: verified cascade */
  IF p_verified IS NOT NULL THEN
    -- If setting verified TRUE, must have at least one linked record
    IF p_verified IS TRUE AND v_document_id IS NULL AND v_transaction_id IS NULL THEN
      RETURN jsonb_build_object('ok', false, 'error', 'no_linked_records');
    END IF;

    -- documents_v2
    IF v_document_id IS NOT NULL THEN
      UPDATE public.documents_v2 d
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE d.document_id = v_document_id;
      affected_docs := FOUND;
    END IF;

    -- transactions
    IF v_transaction_id IS NOT NULL THEN
      UPDATE public.transactions t
         SET reconciled        = p_verified,
             reconciliation_id = CASE WHEN p_verified THEN p_reconciliation_id ELSE NULL END
       WHERE t.transaction_id = v_transaction_id;
      affected_tx := FOUND;
    END IF;

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

    IF v_stage_updated = 0 THEN
      RETURN jsonb_build_object('ok', false, 'error', 'internal', 'message', 'staging_row_not_found');
    END IF;

    -- canonical reconciliations.verified (you asked for this)
    UPDATE public.reconciliations r
       SET verified = p_verified
     WHERE r.reconciliation_id = p_reconciliation_id;
    GET DIAGNOSTICS v_recon_upd = ROW_COUNT;

    j_result := j_result
      || jsonb_build_object(
            'verified', p_verified,
            'affected', jsonb_build_object(
              'documents_v2', affected_docs,
              'transactions', affected_tx
            )
         );
  END IF;

  -- If neither branch executed, treat as bad request
  IF p_column IS NULL AND p_verified IS NULL THEN
    RETURN jsonb_build_object('ok', false, 'error', 'no_op', 'message', 'nothing to update');
  END IF;

  RETURN j_result;

EXCEPTION WHEN OTHERS THEN
  RETURN jsonb_build_object(
    'ok', false,
    'error', 'internal',
    'message', SQLERRM
  );
END;
$$;

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

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

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

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

create or replace function reconciliations_weight_registry_upsert_v2(p_weight_key text, p_weight_value numeric, p_company_id uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row public.reconciliations_weight_registry;
  v_exists boolean := false;
  v_merchant text;
BEGIN
  -- Normalize merchant id (empty -> NULL)
  v_merchant := NULLIF(btrim(p_merchant_id), '');

  -- 1) Safety: company_id is required for frontend calls
  IF p_company_id IS NULL THEN
    RAISE EXCEPTION 'company_id is required for this operation'
      USING ERRCODE = '22023';
  END IF;

  -- 2) Safety: never allow writes to the DEFAULT registry (company_id NULL AND merchant_id NULL)
  --    (Our requirement above already prevents this, but keep the guard explicit.)
  IF p_company_id IS NULL AND v_merchant IS NULL THEN
    RAISE EXCEPTION 'default registry rows are read-only'
      USING ERRCODE = '42501';
  END IF;

  -- 3) Validate key and value
  IF p_weight_key NOT IN ('date','invoice_number','order_number','merchant_id','amount_total','currency','customer_name') THEN
    RAISE EXCEPTION
      'weight_key must be one of (date, invoice_number, order_number, merchant_id, amount_total, currency, customer_name). Got: %',
      p_weight_key USING ERRCODE = '22023';
  END IF;

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

  -- 4) Check if the row already exists for this (company, merchant, key)
  SELECT TRUE
    INTO v_exists
  FROM public.reconciliations_weight_registry
  WHERE company_id = p_company_id
    AND ((merchant_id IS NULL AND v_merchant IS NULL) OR merchant_id = v_merchant)
    AND weight_key = p_weight_key
  LIMIT 1;

  -- 5) If caller wants to "remove", we soft-delete (active=false) ONLY if it exists.
  IF p_active IS FALSE THEN
    IF NOT v_exists THEN
      RAISE EXCEPTION 'cannot deactivate: row does not exist for (company_id %, merchant_id %, key %)',
        p_company_id, COALESCE(v_merchant,'NULL'), p_weight_key USING ERRCODE = '02000';
    END IF;

    UPDATE public.reconciliations_weight_registry
       SET active = FALSE
     WHERE company_id = p_company_id
       AND ((merchant_id IS NULL AND v_merchant IS NULL) OR merchant_id = v_merchant)
       AND weight_key = p_weight_key
     RETURNING * INTO v_row;

    RETURN v_row;
  END IF;

  -- 6) Upsert (insert or update) for active=true
  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_merchant, p_weight_key, p_weight_value, TRUE)
  ON CONFLICT (company_id, merchant_id, weight_key)
  DO UPDATE SET weight_value = EXCLUDED.weight_value,
                active       = TRUE
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v13(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v13'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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;

  /* 1) Normalize sources into temp tables + indexes */
  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,

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

    public.norm_text_ns(t.name_consumer) AS consumer_core_ns,

    NULLIF(regexp_replace(UPPER(COALESCE(t.iban_counterparty, t.iban_beneficiary, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_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 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,
    public.norm_core_ns(d.merchant_name)       AS merch_core_ns,
    public.norm_text_ns(d.customer_info::text) AS custinfo_core_ns,
    NULLIF(regexp_replace(UPPER((COALESCE(d.merchant_info::text,'') || ' ' || COALESCE(d.customer_info::text,''))), '[^A-Z0-9]', '', 'g'), '') AS info_ns
  FROM public.documents_v2 d
  WHERE COALESCE(d.reconciled, FALSE) = FALSE
    AND (p_company_id IS NULL OR d.company_id = p_company_id);

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

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

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

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

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

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

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

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_iban_ns IS NOT NULL
     AND length(tx.tx_iban_ns) >= 15
     AND position(tx.tx_iban_ns in d.info_ns) > 0;

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

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

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

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

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

  CREATE INDEX ON _rw (transaction_id, document_id);

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

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

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

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

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

  GET DIAGNOSTICS v_rows = ROW_COUNT;

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

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

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

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

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

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

create or replace function reconciliations_weight_registry_upsert_v3(p_weight_key text, p_weight_value numeric, p_company_id uuid, p_merchant_id text DEFAULT NULL::text, p_active boolean DEFAULT true) returns reconciliations_weight_registry
    language plpgsql
as
$$
DECLARE
  v_row    public.reconciliations_weight_registry;
  v_mer    text := NULLIF(btrim(p_merchant_id), '');
  v_exists boolean;
BEGIN
  IF p_company_id IS NULL THEN
    RAISE EXCEPTION 'company_id is required' USING ERRCODE='22023';
  END IF;

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

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

  -- Do NOT allow touches to global defaults (company_id NULL AND merchant_id NULL)
  -- Already enforced by requiring p_company_id, but keep explicit guard:
  -- (no-op here since p_company_id is NOT NULL)

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

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

  -- Single UPSERT on the real UNIQUE constraint (handles NULL merchant via merchant_id_norm)
  INSERT INTO public.reconciliations_weight_registry (
      company_id, merchant_id, weight_key, weight_value, active
  )
  VALUES (p_company_id, v_mer, p_weight_key, p_weight_value, p_active)
  ON CONFLICT ON CONSTRAINT uq_weight_registry_company_merchantnorm_key
  DO UPDATE SET
      weight_value = EXCLUDED.weight_value,
      active       = EXCLUDED.active
  RETURNING * INTO v_row;

  RETURN v_row;
END;
$$;

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

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

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

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

create or replace function athena_reconcile_general_v14(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v14'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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;

  /* 1) Normalize sources into temp tables + indexes */
  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, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_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 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(d.merchant_name), '') AS merch_core_ns,

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

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

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

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

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

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

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

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

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

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

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

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

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

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

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

  CREATE INDEX ON _rw (transaction_id, document_id);

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

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

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

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

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

  GET DIAGNOSTICS v_rows = ROW_COUNT;

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

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

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

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

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

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

create or replace function athena_reconcile_general_v15(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v15'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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;

  /* 1) Normalize sources into temp tables + indexes */
  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, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_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 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,
    -- FIX: use merchant_name or merchant_info->>'name'
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

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

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

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

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

  /* 2) Candidates via targeted branches */
  -- [unchanged candidate tables logic from v14] --

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   AND d.total_num = tx.debited_num;

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

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

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

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

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

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

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

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

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

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

  CREATE INDEX ON _rw (transaction_id, document_id);

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

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

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

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

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

  GET DIAGNOSTICS v_rows = ROW_COUNT;

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

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

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

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

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

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

create or replace function athena_reconcile_general_v16(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v16'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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;

  /* 1) Normalize sources into temp tables + indexes */
  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, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    /* NEW: normalized tx info blob (description + remittance) */
    NULLIF(
      regexp_replace(
        UPPER(COALESCE(t.description,'') || ' ' || COALESCE(t.remittance_information,'')),
        '[^A-Z0-9]', '', 'g'
      ),
      ''
    ) AS info_tx_ns,

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

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

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

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

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

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

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

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

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

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

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

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

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

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.tx_bic_ns IS NOT NULL
     AND position(tx.tx_bic_ns in d.info_ns) > 0;

  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.custinfo_core_ns, d.info_ns
  FROM _tx tx
  JOIN _docs d
    ON tx.company_id_txt = d.company_id_uuid::text
   WHERE tx.completed_date IS NOT NULL
     AND d.document_unix  IS NOT NULL
     AND abs((d.document_unix - tx.completed_date)/86400.0) <= p_date_window_days;

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

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

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

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

  CREATE INDEX ON _rw (transaction_id, document_id);

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

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

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

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

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

  GET DIAGNOSTICS v_rows = ROW_COUNT;

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

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

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

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

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

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

create or replace function athena_reconcile_general_v17(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v17'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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;

  /* 1) Normalize sources into temp tables + indexes */
  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, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

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

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

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

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

    /* merchant name from field or JSON */
    NULLIF(public.norm_core_ns(
      COALESCE(d.merchant_name, d.merchant_info::jsonb ->> 'name')
    ), '') AS merch_core_ns,

    /* precise seller IBAN/BIC for safe checks */
    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,

    /* customer normalized block */
    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,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  CREATE INDEX ON _rw (transaction_id, document_id);

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

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

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

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

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

  GET DIAGNOSTICS v_rows = ROW_COUNT;

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

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

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

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

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

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

create or replace function athena_reconcile_general_v18(p_weightset_id uuid DEFAULT NULL::uuid, p_athena_view text DEFAULT 'athena_reconcile_general_v18'::text, p_date_window_days numeric DEFAULT 1, p_min_confidence numeric DEFAULT 0, p_company_id uuid DEFAULT NULL::uuid, p_debug boolean DEFAULT false, p_chunk_limit integer DEFAULT NULL::integer) returns jsonb
    language plpgsql
as
$$
DECLARE
  t0 timestamptz := clock_timestamp();
  t1 timestamptz;
  t2 timestamptz;
  v_rows integer := 0;
  v_status text := 'ok';
  v_msg    text := 'success';
  j_timings jsonb := '{}'::jsonb;
  _count bigint;
BEGIN
  /* 0) Weight 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;

  /* 1) Normalize sources into temp tables + indexes */
  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, t.iban)), '[^A-Z0-9]', '', 'g'), '') AS tx_iban_ns,
    NULLIF(regexp_replace(UPPER(COALESCE(t.bic_counterparty, t.bic, t.bic_beneficiary, t.bic_consumer)), '[^A-Z0-9]', '', 'g'), '') AS tx_bic_ns,

    /* tx info blob for merchant fallback */
    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);

