-- sql/10_maintenance.sql
-- Maintenance and diagnostic functions
-- Provides: register_verify, register_rebuild_totals, register_rebuild_cache, register_stats

-- ============================================================
-- REGISTER_VERIFY: Verify data consistency of a register
-- Compares balance_cache with actual SUM of movements,
-- and checks totals_month / totals_year against movements.
--
-- Returns SETOF (check_type, dim_hash, expected, actual, status)
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_verify(p_name text)
RETURNS TABLE(
    check_type text,
    dim_hash   bigint,
    period     date,
    expected   jsonb,
    actual     jsonb,
    status     text
)
LANGUAGE plpgsql STABLE AS $$
DECLARE
    reg          record;
    res_key      text;
    res_cols     text := '';
    res_sum_cols text := '';
    res_case     text := '';
    first_res    boolean := true;
    dim_cols     text := '';
    first_dim    boolean := true;
    dim_key      text;
BEGIN
    -- 1. Get register metadata
    SELECT * INTO reg FROM @extschema@._registers r WHERE r.name = p_name;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Register "%" does not exist', p_name;
    END IF;

    -- 2. Build column expressions
    FOR dim_key IN SELECT key FROM jsonb_each_text(reg.dimensions) ORDER BY key
    LOOP
        IF NOT first_dim THEN dim_cols := dim_cols || ', '; END IF;
        dim_cols := dim_cols || format('%I', dim_key);
        first_dim := false;
    END LOOP;

    FOR res_key IN SELECT key FROM jsonb_each_text(reg.resources) ORDER BY key
    LOOP
        IF NOT first_res THEN
            res_cols     := res_cols     || ', ';
            res_sum_cols := res_sum_cols || ', ';
        END IF;
        res_cols     := res_cols     || format('%I', res_key);
        res_sum_cols := res_sum_cols || format('SUM(%I) AS %I', res_key, res_key);
        first_res := false;
    END LOOP;

    -- 3. Verify balance_cache (only for balance kind)
    IF reg.kind = 'balance' THEN
        IF NOT reg.high_write THEN
            -- Standard mode: cache should equal SUM(movements)
            RETURN QUERY EXECUTE format(
                'WITH actual AS (
                    SELECT dim_hash, %s
                    FROM @extschema@.%I
                    GROUP BY dim_hash
                ),
                cached AS (
                    SELECT dim_hash, %s
                    FROM @extschema@.%I
                )
                SELECT
                    ''balance_cache''::text AS check_type,
                    COALESCE(a.dim_hash, c.dim_hash) AS dim_hash,
                    NULL::date AS period,
                    (SELECT jsonb_object_agg(k, v) FROM (
                        SELECT * FROM jsonb_each(to_jsonb(c) - ''dim_hash'' - ''last_movement_at'' - ''last_movement_id'' - ''version'' %s)
                    ) x(k,v)) AS expected,
                    (SELECT jsonb_object_agg(k, v) FROM (
                        SELECT * FROM jsonb_each(to_jsonb(a) - ''dim_hash'')
                    ) x(k,v)) AS actual,
                    CASE
                        WHEN c.dim_hash IS NULL THEN ''MISSING_IN_CACHE''
                        WHEN a.dim_hash IS NULL THEN ''ORPHAN_IN_CACHE''
                        WHEN to_jsonb(c) - ''dim_hash'' - ''last_movement_at'' - ''last_movement_id'' - ''version'' %s
                             = to_jsonb(a) - ''dim_hash'' THEN ''OK''
                        ELSE ''MISMATCH''
                    END AS status
                FROM actual a
                FULL OUTER JOIN cached c USING (dim_hash)',
                res_sum_cols,
                p_name || '_movements',
                res_cols,
                p_name || '_balance_cache',
                (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions)),
                (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions))
            );
        ELSE
            -- High-write mode: effective balance = cache + SUM(delta buffer)
            RETURN QUERY EXECUTE format(
                'WITH actual AS (
                    SELECT dim_hash, %s
                    FROM @extschema@.%I
                    GROUP BY dim_hash
                ),
                cached_combined AS (
                    SELECT dim_hash, %s FROM (
                        SELECT dim_hash, %s FROM @extschema@.%I
                        UNION ALL
                        SELECT dim_hash, %s FROM @extschema@.%I
                    ) _all
                    GROUP BY dim_hash
                )
                SELECT
                    ''balance_cache''::text AS check_type,
                    COALESCE(a.dim_hash, cc.dim_hash) AS dim_hash,
                    NULL::date AS period,
                    (SELECT jsonb_object_agg(k, v) FROM (
                        SELECT * FROM jsonb_each(to_jsonb(cc) - ''dim_hash'')
                    ) x(k,v)) AS expected,
                    (SELECT jsonb_object_agg(k, v) FROM (
                        SELECT * FROM jsonb_each(to_jsonb(a) - ''dim_hash'')
                    ) x(k,v)) AS actual,
                    CASE
                        WHEN cc.dim_hash IS NULL THEN ''MISSING_IN_CACHE''
                        WHEN a.dim_hash IS NULL THEN ''ORPHAN_IN_CACHE''
                        WHEN to_jsonb(cc) - ''dim_hash''
                             = to_jsonb(a) - ''dim_hash'' THEN ''OK''
                        ELSE ''MISMATCH''
                    END AS status
                FROM actual a
                FULL OUTER JOIN cached_combined cc USING (dim_hash)',
                res_sum_cols,
                p_name || '_movements',
                res_sum_cols,
                res_cols, p_name || '_balance_cache',
                res_cols, p_name || '_balance_cache_delta'
            );
        END IF;
    END IF;

    -- 4. Verify totals_day
    RETURN QUERY EXECUTE format(
        'WITH actual AS (
            SELECT dim_hash, period::date AS period, %s
            FROM @extschema@.%I
            GROUP BY dim_hash, period::date
        ),
        stored AS (
            SELECT dim_hash, period, %s
            FROM @extschema@.%I
        )
        SELECT
            ''totals_day''::text AS check_type,
            COALESCE(a.dim_hash, s.dim_hash) AS dim_hash,
            COALESCE(a.period, s.period) AS period,
            (SELECT jsonb_object_agg(k, v) FROM (
                SELECT * FROM jsonb_each(to_jsonb(s) - ''dim_hash'' - ''period'' %s)
            ) x(k,v)) AS expected,
            (SELECT jsonb_object_agg(k, v) FROM (
                SELECT * FROM jsonb_each(to_jsonb(a) - ''dim_hash'' - ''period'')
            ) x(k,v)) AS actual,
            CASE
                WHEN s.dim_hash IS NULL THEN ''MISSING_IN_TOTALS''
                WHEN a.dim_hash IS NULL THEN ''ORPHAN_IN_TOTALS''
                WHEN to_jsonb(s) - ''dim_hash'' - ''period'' %s
                     = to_jsonb(a) - ''dim_hash'' - ''period'' THEN ''OK''
                ELSE ''MISMATCH''
            END AS status
        FROM actual a
        FULL OUTER JOIN stored s USING (dim_hash, period)',
        res_sum_cols,
        p_name || '_movements',
        res_cols,
        p_name || '_totals_day',
        (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions)),
        (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions))
    );

    -- 5. Verify totals_month
    RETURN QUERY EXECUTE format(
        'WITH actual AS (
            SELECT dim_hash, date_trunc(''month'', period)::date AS period, %s
            FROM @extschema@.%I
            GROUP BY dim_hash, date_trunc(''month'', period)::date
        ),
        stored AS (
            SELECT dim_hash, period, %s
            FROM @extschema@.%I
        )
        SELECT
            ''totals_month''::text AS check_type,
            COALESCE(a.dim_hash, s.dim_hash) AS dim_hash,
            COALESCE(a.period, s.period) AS period,
            (SELECT jsonb_object_agg(k, v) FROM (
                SELECT * FROM jsonb_each(to_jsonb(s) - ''dim_hash'' - ''period'' %s)
            ) x(k,v)) AS expected,
            (SELECT jsonb_object_agg(k, v) FROM (
                SELECT * FROM jsonb_each(to_jsonb(a) - ''dim_hash'' - ''period'')
            ) x(k,v)) AS actual,
            CASE
                WHEN s.dim_hash IS NULL THEN ''MISSING_IN_TOTALS''
                WHEN a.dim_hash IS NULL THEN ''ORPHAN_IN_TOTALS''
                WHEN to_jsonb(s) - ''dim_hash'' - ''period'' %s
                     = to_jsonb(a) - ''dim_hash'' - ''period'' THEN ''OK''
                ELSE ''MISMATCH''
            END AS status
        FROM actual a
        FULL OUTER JOIN stored s USING (dim_hash, period)',
        res_sum_cols,
        p_name || '_movements',
        res_cols,
        p_name || '_totals_month',
        (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions)),
        (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions))
    );

    -- 6. Verify totals_year against totals_month
    RETURN QUERY EXECUTE format(
        'WITH actual AS (
            SELECT dim_hash, date_trunc(''year'', period)::date AS period, %s
            FROM @extschema@.%I
            GROUP BY dim_hash, date_trunc(''year'', period)::date
        ),
        stored AS (
            SELECT dim_hash, period, %s
            FROM @extschema@.%I
        )
        SELECT
            ''totals_year''::text AS check_type,
            COALESCE(a.dim_hash, s.dim_hash) AS dim_hash,
            COALESCE(a.period, s.period) AS period,
            (SELECT jsonb_object_agg(k, v) FROM (
                SELECT * FROM jsonb_each(to_jsonb(s) - ''dim_hash'' - ''period'' %s)
            ) x(k,v)) AS expected,
            (SELECT jsonb_object_agg(k, v) FROM (
                SELECT * FROM jsonb_each(to_jsonb(a) - ''dim_hash'' - ''period'')
            ) x(k,v)) AS actual,
            CASE
                WHEN s.dim_hash IS NULL THEN ''MISSING_IN_TOTALS''
                WHEN a.dim_hash IS NULL THEN ''ORPHAN_IN_TOTALS''
                WHEN to_jsonb(s) - ''dim_hash'' - ''period'' %s
                     = to_jsonb(a) - ''dim_hash'' - ''period'' THEN ''OK''
                ELSE ''MISMATCH''
            END AS status
        FROM actual a
        FULL OUTER JOIN stored s USING (dim_hash, period)',
        res_sum_cols,
        p_name || '_totals_month',
        res_cols,
        p_name || '_totals_year',
        (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions)),
        (SELECT string_agg(format(' - %L', key), '') FROM jsonb_each_text(reg.dimensions))
    );
END;
$$;

COMMENT ON FUNCTION @extschema@.register_verify(text) IS
    'Verify data consistency: compare balance_cache, totals_day, totals_month, totals_year against actual movements';


-- ============================================================
-- REGISTER_REBUILD_TOTALS: Full rebuild of totals from movements
-- Truncates and re-aggregates totals_day, totals_month and totals_year.
--
-- Returns: number of rebuilt rows (day + month + year)
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_rebuild_totals(p_name text)
RETURNS int
LANGUAGE plpgsql AS $$
DECLARE
    reg          record;
    dim_cols     text := '';
    res_cols     text := '';
    res_sum_cols text := '';
    first_dim    boolean := true;
    first_res    boolean := true;
    dim_key      text;
    res_key      text;
    day_count    int;
    month_count  int;
    year_count   int;
BEGIN
    -- 1. Get register metadata
    SELECT * INTO reg FROM @extschema@._registers r WHERE r.name = p_name;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Register "%" does not exist', p_name;
    END IF;

    -- 2. Build column expressions
    FOR dim_key IN SELECT key FROM jsonb_each_text(reg.dimensions) ORDER BY key
    LOOP
        IF NOT first_dim THEN dim_cols := dim_cols || ', '; END IF;
        dim_cols := dim_cols || format('%I', dim_key);
        first_dim := false;
    END LOOP;

    FOR res_key IN SELECT key FROM jsonb_each_text(reg.resources) ORDER BY key
    LOOP
        IF NOT first_res THEN
            res_cols     := res_cols     || ', ';
            res_sum_cols := res_sum_cols || ', ';
        END IF;
        res_cols     := res_cols     || format('%I', res_key);
        res_sum_cols := res_sum_cols || format('SUM(%I) AS %I', res_key, res_key);
        first_res := false;
    END LOOP;

    -- 3. Bypass protection triggers for rebuild
    PERFORM set_config('pg_accumulator.allow_internal', 'on', true);

    -- 4. Truncate and rebuild totals_day
    EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_totals_day');

    EXECUTE format(
        'INSERT INTO @extschema@.%I (dim_hash, period, %s, %s)
         SELECT dim_hash, period::date, %s, %s
         FROM @extschema@.%I
         GROUP BY dim_hash, period::date, %s',
        p_name || '_totals_day',
        dim_cols, res_cols,
        dim_cols, res_sum_cols,
        p_name || '_movements',
        dim_cols
    );
    GET DIAGNOSTICS day_count = ROW_COUNT;

    -- 5. Truncate and rebuild totals_month from totals_day
    EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_totals_month');

    EXECUTE format(
        'INSERT INTO @extschema@.%I (dim_hash, period, %s, %s)
         SELECT dim_hash, date_trunc(''month'', period)::date, %s, %s
         FROM @extschema@.%I
         GROUP BY dim_hash, date_trunc(''month'', period)::date, %s',
        p_name || '_totals_month',
        dim_cols, res_cols,
        dim_cols, res_sum_cols,
        p_name || '_totals_day',
        dim_cols
    );
    GET DIAGNOSTICS month_count = ROW_COUNT;

    -- 6. Truncate and rebuild totals_year from totals_month
    EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_totals_year');

    EXECUTE format(
        'INSERT INTO @extschema@.%I (dim_hash, period, %s, %s)
         SELECT dim_hash, date_trunc(''year'', period)::date, %s, %s
         FROM @extschema@.%I
         GROUP BY dim_hash, date_trunc(''year'', period)::date, %s',
        p_name || '_totals_year',
        dim_cols, res_cols,
        dim_cols, res_sum_cols,
        p_name || '_totals_month',
        dim_cols
    );
    GET DIAGNOSTICS year_count = ROW_COUNT;

    -- Reset protection bypass
    PERFORM set_config('pg_accumulator.allow_internal', '', true);

    RETURN day_count + month_count + year_count;
END;
$$;

COMMENT ON FUNCTION @extschema@.register_rebuild_totals(text) IS
    'Rebuild totals_day, totals_month and totals_year from movements data';


-- ============================================================
-- REGISTER_REBUILD_CACHE: Rebuild balance_cache from movements
-- Full (dim_hash IS NULL) or partial (specific dim_hash).
--
-- Returns: number of rebuilt cache rows
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_rebuild_cache(
    p_name     text,
    p_dim_hash bigint DEFAULT NULL
)
RETURNS int
LANGUAGE plpgsql AS $$
DECLARE
    reg          record;
    dim_cols     text := '';
    res_cols     text := '';
    res_sum_cols text := '';
    first_dim    boolean := true;
    first_res    boolean := true;
    dim_key      text;
    res_key      text;
    rebuilt      int;
BEGIN
    -- 1. Get register metadata
    SELECT * INTO reg FROM @extschema@._registers r WHERE r.name = p_name;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Register "%" does not exist', p_name;
    END IF;

    IF reg.kind != 'balance' THEN
        RAISE EXCEPTION 'rebuild_cache is only available for balance-type registers, "%" is %',
            p_name, reg.kind;
    END IF;

    -- 2. Build column expressions
    FOR dim_key IN SELECT key FROM jsonb_each_text(reg.dimensions) ORDER BY key
    LOOP
        IF NOT first_dim THEN dim_cols := dim_cols || ', '; END IF;
        dim_cols := dim_cols || format('%I', dim_key);
        first_dim := false;
    END LOOP;

    FOR res_key IN SELECT key FROM jsonb_each_text(reg.resources) ORDER BY key
    LOOP
        IF NOT first_res THEN
            res_cols     := res_cols     || ', ';
            res_sum_cols := res_sum_cols || ', ';
        END IF;
        res_cols     := res_cols     || format('%I', res_key);
        res_sum_cols := res_sum_cols || format('SUM(%I) AS %I', res_key, res_key);
        first_res := false;
    END LOOP;

    -- 3. Bypass protection triggers for rebuild
    PERFORM set_config('pg_accumulator.allow_internal', 'on', true);

    IF p_dim_hash IS NULL THEN
        -- Full rebuild
        EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_balance_cache');

        -- Also flush delta buffer if high_write
        IF reg.high_write THEN
            EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_balance_cache_delta');
        END IF;

        EXECUTE format(
            'INSERT INTO @extschema@.%I (dim_hash, %s, %s, last_movement_at, last_movement_id)
             SELECT dim_hash, %s, %s,
                    MAX(recorded_at),
                    (array_agg(id ORDER BY recorded_at DESC))[1]
             FROM @extschema@.%I
             GROUP BY dim_hash, %s',
            p_name || '_balance_cache',
            dim_cols, res_cols,
            dim_cols, res_sum_cols,
            p_name || '_movements',
            dim_cols
        );
    ELSE
        -- Partial rebuild: specific dim_hash
        EXECUTE format('DELETE FROM @extschema@.%I WHERE dim_hash = $1',
            p_name || '_balance_cache')
            USING p_dim_hash;

        -- Flush deltas for this dim_hash if high_write
        IF reg.high_write THEN
            EXECUTE format('DELETE FROM @extschema@.%I WHERE dim_hash = $1',
                p_name || '_balance_cache_delta')
                USING p_dim_hash;
        END IF;

        EXECUTE format(
            'INSERT INTO @extschema@.%I (dim_hash, %s, %s, last_movement_at, last_movement_id)
             SELECT dim_hash, %s, %s,
                    MAX(recorded_at),
                    (array_agg(id ORDER BY recorded_at DESC))[1]
             FROM @extschema@.%I
             WHERE dim_hash = $1
             GROUP BY dim_hash, %s',
            p_name || '_balance_cache',
            dim_cols, res_cols,
            dim_cols, res_sum_cols,
            p_name || '_movements',
            dim_cols
        ) USING p_dim_hash;
    END IF;

    GET DIAGNOSTICS rebuilt = ROW_COUNT;

    -- Reset protection bypass
    PERFORM set_config('pg_accumulator.allow_internal', '', true);

    RETURN rebuilt;
END;
$$;

COMMENT ON FUNCTION @extschema@.register_rebuild_cache(text, bigint) IS
    'Rebuild balance_cache from movements. Full rebuild if dim_hash IS NULL, partial otherwise';


-- ============================================================
-- REGISTER_STATS: Collect register statistics
-- Returns a JSON object with counts and table sizes.
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_stats(p_name text)
RETURNS jsonb
LANGUAGE plpgsql STABLE AS $$
DECLARE
    reg               record;
    result            jsonb;
    v_movements       bigint := 0;
    v_partitions      int    := 0;
    v_cache_rows      bigint := 0;
    v_day_rows        bigint := 0;
    v_month_rows      bigint := 0;
    v_year_rows       bigint := 0;
    v_delta_pending   bigint := 0;
    v_last_delta      timestamptz;
    v_table_sizes     jsonb;
    v_movements_size  text;
    v_day_size        text;
    v_month_size      text;
    v_year_size       text;
    v_cache_size      text;
BEGIN
    -- 1. Get register metadata
    SELECT * INTO reg FROM @extschema@._registers r WHERE r.name = p_name;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Register "%" does not exist', p_name;
    END IF;

    -- 2. Count movements
    BEGIN
        EXECUTE format('SELECT count(*) FROM @extschema@.%I',
            p_name || '_movements') INTO v_movements;
    EXCEPTION WHEN undefined_table THEN
        v_movements := 0;
    END;

    -- 3. Count partitions
    SELECT count(*)::int INTO v_partitions
    FROM pg_inherits i
    JOIN pg_class parent ON i.inhparent = parent.oid
    JOIN pg_class child ON i.inhrelid = child.oid
    JOIN pg_namespace ns ON parent.relnamespace = ns.oid
    WHERE parent.relname = p_name || '_movements'
      AND ns.nspname = (SELECT nspname FROM pg_namespace WHERE oid = (
          SELECT relnamespace FROM pg_class WHERE relname = p_name || '_movements'
          AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = '@extschema@')
      ));

    -- Fallback: try with accum schema directly for test environment
    IF v_partitions = 0 THEN
        SELECT count(*)::int INTO v_partitions
        FROM pg_inherits i
        JOIN pg_class parent ON i.inhparent = parent.oid
        JOIN pg_namespace ns ON parent.relnamespace = ns.oid
        WHERE parent.relname = p_name || '_movements'
          AND ns.nspname = 'accum';
    END IF;

    -- 4. Count totals rows
    BEGIN
        EXECUTE format('SELECT count(*) FROM @extschema@.%I',
            p_name || '_totals_day') INTO v_day_rows;
    EXCEPTION WHEN undefined_table THEN
        v_day_rows := 0;
    END;

    BEGIN
        EXECUTE format('SELECT count(*) FROM @extschema@.%I',
            p_name || '_totals_month') INTO v_month_rows;
    EXCEPTION WHEN undefined_table THEN
        v_month_rows := 0;
    END;

    BEGIN
        EXECUTE format('SELECT count(*) FROM @extschema@.%I',
            p_name || '_totals_year') INTO v_year_rows;
    EXCEPTION WHEN undefined_table THEN
        v_year_rows := 0;
    END;

    -- 5. Count balance_cache rows (if applicable)
    IF reg.kind = 'balance' THEN
        BEGIN
            EXECUTE format('SELECT count(*) FROM @extschema@.%I',
                p_name || '_balance_cache') INTO v_cache_rows;
        EXCEPTION WHEN undefined_table THEN
            v_cache_rows := 0;
        END;
    END IF;

    -- 6. Delta buffer info (if high_write)
    IF reg.high_write THEN
        BEGIN
            EXECUTE format('SELECT count(*) FROM @extschema@.%I',
                p_name || '_balance_cache_delta') INTO v_delta_pending;
            EXECUTE format('SELECT MAX(created_at) FROM @extschema@.%I',
                p_name || '_balance_cache_delta') INTO v_last_delta;
        EXCEPTION WHEN undefined_table THEN
            v_delta_pending := 0;
            v_last_delta := NULL;
        END;
    END IF;

    -- 7. Table sizes
    BEGIN
        SELECT pg_size_pretty(pg_total_relation_size(
            format('@extschema@.%I', p_name || '_movements')::regclass))
            INTO v_movements_size;
    EXCEPTION WHEN OTHERS THEN
        v_movements_size := '0 bytes';
    END;

    BEGIN
        SELECT pg_size_pretty(pg_total_relation_size(
            format('@extschema@.%I', p_name || '_totals_day')::regclass))
            INTO v_day_size;
    EXCEPTION WHEN OTHERS THEN
        v_day_size := '0 bytes';
    END;

    BEGIN
        SELECT pg_size_pretty(pg_total_relation_size(
            format('@extschema@.%I', p_name || '_totals_month')::regclass))
            INTO v_month_size;
    EXCEPTION WHEN OTHERS THEN
        v_month_size := '0 bytes';
    END;

    BEGIN
        SELECT pg_size_pretty(pg_total_relation_size(
            format('@extschema@.%I', p_name || '_totals_year')::regclass))
            INTO v_year_size;
    EXCEPTION WHEN OTHERS THEN
        v_year_size := '0 bytes';
    END;

    v_table_sizes := jsonb_build_object(
        'movements', v_movements_size,
        'totals_day', v_day_size,
        'totals_month', v_month_size,
        'totals_year', v_year_size
    );

    IF reg.kind = 'balance' THEN
        BEGIN
            SELECT pg_size_pretty(pg_total_relation_size(
                format('@extschema@.%I', p_name || '_balance_cache')::regclass))
                INTO v_cache_size;
        EXCEPTION WHEN OTHERS THEN
            v_cache_size := '0 bytes';
        END;
        v_table_sizes := v_table_sizes || jsonb_build_object('balance_cache', v_cache_size);
    END IF;

    -- 8. Build result
    result := jsonb_build_object(
        'movements_count',      v_movements,
        'partitions_count',     v_partitions,
        'cache_rows',           v_cache_rows,
        'totals_day_rows',      v_day_rows,
        'totals_month_rows',    v_month_rows,
        'totals_year_rows',     v_year_rows,
        'delta_buffer_pending', v_delta_pending,
        'last_delta_merge',     v_last_delta,
        'table_sizes',          v_table_sizes
    );

    RETURN result;
END;
$$;

COMMENT ON FUNCTION @extschema@.register_stats(text) IS
    'Collect register statistics: row counts, partition count, table sizes';


-- ============================================================
-- _RECOVERY_CHECK: Post-crash consistency recovery
-- Called by bgworker on startup. For each high_write balance register,
-- checks if the UNLOGGED delta buffer was lost (truncated by crash)
-- and rebuilds balance_cache from movements if needed.
--
-- Returns: number of registers that were rebuilt
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@._recovery_check()
RETURNS int
LANGUAGE plpgsql AS $$
DECLARE
    reg          record;
    v_delta_cnt  bigint;
    v_cache_cnt  bigint;
    v_mov_cnt    bigint;
    v_mismatch   bigint;
    rebuilt      int := 0;
BEGIN
    FOR reg IN
        SELECT r.name FROM @extschema@._registers r
        WHERE r.high_write = true AND r.kind = 'balance'
        ORDER BY r.name
    LOOP
        -- Check if delta buffer is empty (possible crash truncation)
        EXECUTE format('SELECT count(*) FROM @extschema@.%I',
            reg.name || '_balance_cache_delta') INTO v_delta_cnt;

        -- Only investigate if delta buffer is empty (normal state after merge too,
        -- so also check for actual mismatch)
        IF v_delta_cnt = 0 THEN
            EXECUTE format('SELECT count(*) FROM @extschema@.%I',
                reg.name || '_balance_cache') INTO v_cache_cnt;
            EXECUTE format('SELECT count(*) FROM @extschema@.%I',
                reg.name || '_movements') INTO v_mov_cnt;

            -- Quick sanity: if movements exist but cache is empty, rebuild
            IF v_mov_cnt > 0 AND v_cache_cnt = 0 THEN
                PERFORM @extschema@.register_rebuild_cache(reg.name);
                rebuilt := rebuilt + 1;
                RAISE LOG 'pg_accumulator recovery: rebuilt cache for register "%" (missing cache)',
                    reg.name;
                CONTINUE;
            END IF;

            -- Check for actual mismatch count (lightweight: just count mismatches)
            IF v_cache_cnt > 0 THEN
                SELECT count(*) INTO v_mismatch
                FROM @extschema@.register_verify(reg.name)
                WHERE check_type = 'balance_cache' AND status != 'OK';

                IF v_mismatch > 0 THEN
                    PERFORM @extschema@.register_rebuild_cache(reg.name);
                    rebuilt := rebuilt + 1;
                    RAISE LOG 'pg_accumulator recovery: rebuilt cache for register "%" (% mismatches)',
                        reg.name, v_mismatch;
                END IF;
            END IF;
        END IF;
    END LOOP;

    RETURN rebuilt;
END;
$$;

COMMENT ON FUNCTION @extschema@._recovery_check() IS
    'Post-crash recovery: detect and rebuild inconsistent high_write balance caches';
