-- sql/07_registry_api.sql
-- Public registry management API
-- Orchestrates DDL, triggers, and hash generation

-- ============================================================
-- REGISTER_CREATE: Creates a new accumulation register
-- Orchestrates: validation → metadata → DDL → hash → triggers
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_create(
    name          text,
    dimensions    jsonb,
    resources     jsonb,
    kind          text    DEFAULT 'balance',
    totals_period text    DEFAULT 'day',
    partition_by  text    DEFAULT 'month',
    high_write    boolean DEFAULT false,
    recorder_type text    DEFAULT 'text'
) RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
    -- 1. Validate inputs
    PERFORM @extschema@._validate_name(name);
    PERFORM @extschema@._validate_dimensions(dimensions);
    PERFORM @extschema@._validate_resources(resources);

    IF kind NOT IN ('balance', 'turnover') THEN
        RAISE EXCEPTION 'Invalid kind: %. Must be balance or turnover', kind;
    END IF;

    IF totals_period NOT IN ('day', 'month', 'year') THEN
        RAISE EXCEPTION 'Invalid totals_period: %', totals_period;
    END IF;

    IF partition_by NOT IN ('day', 'month', 'quarter', 'year') THEN
        RAISE EXCEPTION 'Invalid partition_by: %', partition_by;
    END IF;

    -- Check for duplicates
    IF @extschema@._register_exists(name) THEN
        RAISE EXCEPTION 'Register "%" already exists', name;
    END IF;

    -- 2. Save metadata to registry
    PERFORM @extschema@._register_put(
        name, kind, dimensions, resources,
        totals_period, partition_by, high_write, recorder_type
    );

    -- 3. Generate storage infrastructure (DDL)
    PERFORM @extschema@._ddl_create_movements_table(name, recorder_type, dimensions, resources);
    PERFORM @extschema@._ddl_create_totals_tables(name, dimensions, resources);

    IF kind = 'balance' THEN
        PERFORM @extschema@._ddl_create_balance_cache(name, dimensions, resources);
    END IF;

    IF high_write THEN
        PERFORM @extschema@._ddl_create_delta_buffer(name, resources);
    END IF;

    -- 4. Create indexes
    PERFORM @extschema@._ddl_create_indexes(name, kind, dimensions, high_write);

    -- 5. Generate hash function
    PERFORM @extschema@._generate_hash_function(name, dimensions);

    -- 6. Generate triggers (movement→totals→cache chain)
    PERFORM @extschema@._generate_triggers(name, kind, dimensions, resources, high_write);

    -- 7. Generate read functions (balance/turnover/movements)
    PERFORM @extschema@._generate_read_functions(name, kind, dimensions, resources, high_write);

    -- 8. Create initial partitions
    PERFORM @extschema@._create_initial_partitions(name, partition_by, 3);
END;
$$;

-- ============================================================
-- REGISTER_ALTER: Modify an existing register's structure
-- Supports: adding dimensions, adding resources, toggling high_write
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_alter(
    p_name            text,
    add_dimensions    jsonb    DEFAULT NULL,
    add_resources     jsonb    DEFAULT NULL,
    high_write        boolean  DEFAULT NULL
) RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
    reg             record;
    dim_key         text;
    dim_type        text;
    res_key         text;
    res_type        text;
    new_dimensions  jsonb;
    new_resources   jsonb;
    dims_added      boolean := false;
BEGIN
    -- 1. Get current 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;

    new_dimensions := reg.dimensions;
    new_resources  := reg.resources;

    -- 2. Add new dimensions (requires recalculation)
    IF add_dimensions IS NOT NULL AND add_dimensions != '{}'::jsonb THEN
        PERFORM @extschema@._validate_dimensions(add_dimensions);

        -- Check for conflicts with existing dimensions
        FOR dim_key, dim_type IN SELECT * FROM jsonb_each_text(add_dimensions)
        LOOP
            IF reg.dimensions ? dim_key THEN
                RAISE EXCEPTION 'Dimension "%" already exists in register "%"', dim_key, p_name;
            END IF;

            -- Add column to movements table
            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s',
                p_name || '_movements', dim_key, dim_type);
            -- Add column to totals tables
            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s',
                p_name || '_totals_day', dim_key, dim_type);
            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s',
                p_name || '_totals_month', dim_key, dim_type);
            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s',
                p_name || '_totals_year', dim_key, dim_type);
            -- Add column to balance_cache if it exists
            IF reg.kind = 'balance' THEN
                EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s',
                    p_name || '_balance_cache', dim_key, dim_type);
            END IF;
        END LOOP;

        new_dimensions := reg.dimensions || add_dimensions;
        dims_added := true;
    END IF;

    -- 3. Add new resources (no recalculation needed, DEFAULT 0)
    IF add_resources IS NOT NULL AND add_resources != '{}'::jsonb THEN
        PERFORM @extschema@._validate_resources(add_resources);

        FOR res_key, res_type IN SELECT * FROM jsonb_each_text(add_resources)
        LOOP
            IF reg.resources ? res_key THEN
                RAISE EXCEPTION 'Resource "%" already exists in register "%"', res_key, p_name;
            END IF;

            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s NOT NULL DEFAULT 0',
                p_name || '_movements', res_key, res_type);
            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s NOT NULL DEFAULT 0',
                p_name || '_totals_day', res_key, res_type);
            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s NOT NULL DEFAULT 0',
                p_name || '_totals_month', res_key, res_type);
            EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s NOT NULL DEFAULT 0',
                p_name || '_totals_year', res_key, res_type);
            IF reg.kind = 'balance' THEN
                EXECUTE format('ALTER TABLE @extschema@.%I ADD COLUMN %I %s NOT NULL DEFAULT 0',
                    p_name || '_balance_cache', res_key, res_type);
            END IF;
        END LOOP;

        new_resources := reg.resources || add_resources;
    END IF;

    -- 4. Toggle high_write mode
    IF high_write IS NOT NULL AND high_write IS DISTINCT FROM reg.high_write THEN
        IF high_write AND NOT reg.high_write THEN
            -- Enable high_write: create delta buffer
            PERFORM @extschema@._ddl_create_delta_buffer(p_name, new_resources);
            EXECUTE format('CREATE INDEX ON @extschema@.%I (dim_hash)',
                p_name || '_balance_cache_delta');
            EXECUTE format('CREATE INDEX ON @extschema@.%I (created_at)',
                p_name || '_balance_cache_delta');
        ELSIF NOT high_write AND reg.high_write THEN
            -- Disable high_write: drop delta buffer
            EXECUTE format('DROP TABLE IF EXISTS @extschema@.%I CASCADE',
                p_name || '_balance_cache_delta');
        END IF;
    END IF;

    -- 5. Regenerate hash function with new dimensions
    IF dims_added THEN
        -- Drop old hash function (may have different arg count)
        PERFORM @extschema@._drop_hash_function(p_name);
        PERFORM @extschema@._generate_hash_function(p_name, new_dimensions);

        -- Recalculate dim_hash for existing movements
        DECLARE
            hash_args text := '';
            first_d   boolean := true;
            d_key     text;
        BEGIN
            FOR d_key IN SELECT key FROM jsonb_each_text(new_dimensions) ORDER BY key
            LOOP
                IF NOT first_d THEN hash_args := hash_args || ', '; END IF;
                hash_args := hash_args || format('%I', d_key);
                first_d := false;
            END LOOP;

            -- Bypass protection trigger for internal update
            PERFORM set_config('pg_accumulator.allow_internal', 'on', true);

            EXECUTE format('UPDATE @extschema@.%I SET dim_hash = @extschema@.%I(%s)',
                p_name || '_movements',
                '_hash_' || p_name,
                hash_args
            );
        END;

        -- Rebuild totals and cache from scratch
        EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_totals_day');
        EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_totals_month');
        EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_totals_year');
        IF reg.kind = 'balance' THEN
            EXECUTE format('TRUNCATE @extschema@.%I', p_name || '_balance_cache');
        END IF;

        PERFORM @extschema@._rebuild_totals_from_movements(p_name, reg.kind, new_dimensions, new_resources);
    END IF;

    -- 6. Regenerate triggers (with updated dimensions/resources/high_write)
    IF dims_added OR (add_resources IS NOT NULL AND add_resources != '{}'::jsonb) OR
       (high_write IS NOT NULL AND high_write IS DISTINCT FROM reg.high_write) THEN
        -- Drop old triggers
        EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_before_insert ON @extschema@.%I',
            p_name, p_name || '_movements');
        EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_after_insert ON @extschema@.%I',
            p_name, p_name || '_movements');
        EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_after_delete ON @extschema@.%I',
            p_name, p_name || '_movements');
        EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_block_update ON @extschema@.%I',
            p_name, p_name || '_movements');
        EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_protect_totals_day ON @extschema@.%I',
            p_name, p_name || '_totals_day');
        EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_protect_totals_month ON @extschema@.%I',
            p_name, p_name || '_totals_month');
        EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_protect_totals_year ON @extschema@.%I',
            p_name, p_name || '_totals_year');
        IF reg.kind = 'balance' THEN
            EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_protect_balance_cache ON @extschema@.%I',
                p_name, p_name || '_balance_cache');
        END IF;
        BEGIN
            EXECUTE format('DROP FUNCTION IF EXISTS @extschema@.%I CASCADE',
                '_trg_' || p_name || '_before_insert');
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'Skipping drop of trigger function %: %', '_trg_' || p_name || '_before_insert', SQLERRM;
        END;
        BEGIN
            EXECUTE format('DROP FUNCTION IF EXISTS @extschema@.%I CASCADE',
                '_trg_' || p_name || '_after_insert');
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'Skipping drop of trigger function %: %', '_trg_' || p_name || '_after_insert', SQLERRM;
        END;
        BEGIN
            EXECUTE format('DROP FUNCTION IF EXISTS @extschema@.%I CASCADE',
                '_trg_' || p_name || '_after_delete');
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'Skipping drop of trigger function %: %', '_trg_' || p_name || '_after_delete', SQLERRM;
        END;
        BEGIN
            EXECUTE format('DROP FUNCTION IF EXISTS @extschema@.%I CASCADE',
                '_trg_' || p_name || '_block_update');
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'Skipping drop of trigger function %: %', '_trg_' || p_name || '_block_update', SQLERRM;
        END;
        BEGIN
            EXECUTE format('DROP FUNCTION IF EXISTS @extschema@.%I CASCADE',
                '_trg_' || p_name || '_protect_derived');
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'Skipping drop of trigger function %: %', '_trg_' || p_name || '_protect_derived', SQLERRM;
        END;

        PERFORM @extschema@._generate_triggers(
            p_name, reg.kind, new_dimensions, new_resources,
            coalesce(high_write, reg.high_write)
        );

        -- Regenerate read functions with updated structure
        PERFORM @extschema@._drop_read_functions(p_name);
        PERFORM @extschema@._generate_read_functions(
            p_name, reg.kind, new_dimensions, new_resources,
            coalesce(high_write, reg.high_write)
        );
    END IF;

    -- 7. Update metadata
    PERFORM @extschema@._register_put(
        p_name, reg.kind, new_dimensions, new_resources,
        reg.totals_period, reg.partition_by,
        coalesce(high_write, reg.high_write), reg.recorder_type
    );
END;
$$;

-- ============================================================
-- HELPER: Rebuild totals and cache from movements (used by alter)
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@._rebuild_totals_from_movements(
    p_name       text,
    p_kind       text,
    p_dimensions jsonb,
    p_resources  jsonb
) RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
    dim_cols     text := '';
    res_cols     text := '';
    res_sum_cols text := '';
    res_key      text;
    dim_key      text;
    first_dim    boolean := true;
    first_res    boolean := true;
BEGIN
    FOR dim_key IN SELECT key FROM jsonb_each_text(p_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(p_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;

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

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

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

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

    -- Rebuild balance_cache
    IF p_kind = 'balance' THEN
        EXECUTE format(
            'INSERT INTO @extschema@.%I (dim_hash, %s, %s)
             SELECT dim_hash, %s, %s
             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
        );
    END IF;

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

-- ============================================================
-- REGISTER_DROP: Removes a register and all its infrastructure
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_drop(
    name  text,
    force boolean DEFAULT false
) RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
    reg record;
BEGIN
    -- Check existence
    SELECT * INTO reg FROM @extschema@._registers r WHERE r.name = register_drop.name;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Register "%" does not exist', name;
    END IF;

    -- Check for data if not forced
    IF NOT force THEN
        DECLARE
            cnt bigint;
        BEGIN
            EXECUTE format('SELECT count(*) FROM @extschema@.%I',
                name || '_movements') INTO cnt;
            IF cnt > 0 THEN
                RAISE EXCEPTION 'Register "%" contains % movements. Use force := true to drop.',
                    name, cnt;
            END IF;
        END;
    END IF;

    -- Drop all infrastructure (tables, triggers, functions)
    PERFORM @extschema@._ddl_drop_infrastructure(name);

    -- Remove from registry
    PERFORM @extschema@._register_delete(name);
END;
$$;

-- ============================================================
-- REGISTER_LIST: List all registers with summary info
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_list()
RETURNS TABLE(name text, kind text, dimensions int, resources int, movements_count bigint, created_at timestamptz)
LANGUAGE plpgsql STABLE AS $$
DECLARE
    reg record;
    cnt bigint;
BEGIN
    FOR reg IN SELECT * FROM @extschema@._registers r ORDER BY r.created_at
    LOOP
        BEGIN
            EXECUTE format('SELECT count(*) FROM @extschema@.%I',
                reg.name || '_movements') INTO cnt;
        EXCEPTION WHEN undefined_table THEN
            cnt := 0;
        END;

        name       := reg.name;
        kind       := reg.kind;
        dimensions := (SELECT count(*)::int FROM jsonb_object_keys(reg.dimensions));
        resources  := (SELECT count(*)::int FROM jsonb_object_keys(reg.resources));
        movements_count := cnt;
        created_at := reg.created_at;
        RETURN NEXT;
    END LOOP;
END;
$$;

-- ============================================================
-- REGISTER_INFO: Detailed register information as JSONB
-- Includes structure, statistics, and partition info
-- ============================================================
CREATE OR REPLACE FUNCTION @extschema@.register_info(p_name text)
RETURNS jsonb
LANGUAGE plpgsql STABLE AS $$
DECLARE
    reg           record;
    result        jsonb;
    movements_cnt bigint := 0;
    partitions    jsonb  := '[]'::jsonb;
    tables_info   jsonb;
BEGIN
    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;

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

    -- Partition info
    BEGIN
        SELECT jsonb_agg(jsonb_build_object(
            'name', child.relname,
            'range', pg_get_expr(c.relpartbound, c.oid)
        ) ORDER BY child.relname)
        INTO 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_catalog.pg_class c ON c.oid = child.oid
        JOIN pg_namespace ns ON parent.relnamespace = ns.oid
        WHERE parent.relname = p_name || '_movements'
          AND ns.nspname = '@extschema@';
    EXCEPTION WHEN OTHERS THEN
        partitions := '[]'::jsonb;
    END;

    IF partitions IS NULL THEN
        partitions := '[]'::jsonb;
    END IF;

    -- Tables info
    tables_info := jsonb_build_object(
        'movements', p_name || '_movements',
        'totals_day', p_name || '_totals_day',
        'totals_month', p_name || '_totals_month',
        'totals_year', p_name || '_totals_year'
    );

    IF reg.kind = 'balance' THEN
        tables_info := tables_info || jsonb_build_object(
            'balance_cache', p_name || '_balance_cache');
    END IF;

    IF reg.high_write THEN
        tables_info := tables_info || jsonb_build_object(
            'balance_cache_delta', p_name || '_balance_cache_delta');
    END IF;

    result := jsonb_build_object(
        'name',            reg.name,
        'kind',            reg.kind,
        'dimensions',      reg.dimensions,
        'resources',       reg.resources,
        'totals_period',   reg.totals_period,
        'partition_by',    reg.partition_by,
        'high_write',      reg.high_write,
        'recorder_type',   reg.recorder_type,
        'created_at',      reg.created_at,
        'movements_count', movements_cnt,
        'tables',          tables_info,
        'partitions',      partitions
    );

    RETURN result;
END;
$$;
