-- test/setup/01-registry-table.sql
-- Internal metadata registry for accumulation registers

-- ============================================================
-- REGISTRY TABLE
-- ============================================================

CREATE TABLE IF NOT EXISTS accum._registers (
    name           text PRIMARY KEY,
    kind           text NOT NULL DEFAULT 'balance'
                       CHECK (kind IN ('balance', 'turnover')),
    dimensions     jsonb NOT NULL,
    resources      jsonb NOT NULL,
    totals_period  text NOT NULL DEFAULT 'day'
                       CHECK (totals_period IN ('day', 'month', 'year')),
    partition_by   text NOT NULL DEFAULT 'month'
                       CHECK (partition_by IN ('day', 'month', 'quarter', 'year')),
    high_write     boolean NOT NULL DEFAULT false,
    recorder_type  text NOT NULL DEFAULT 'text',
    created_at     timestamptz NOT NULL DEFAULT now(),
    updated_at     timestamptz NOT NULL DEFAULT now()
);

-- ============================================================
-- VALIDATION FUNCTIONS
-- ============================================================

CREATE OR REPLACE FUNCTION accum._validate_name(p_name text)
RETURNS void
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
    IF p_name IS NULL OR p_name = '' THEN
        RAISE EXCEPTION 'Register name cannot be empty';
    END IF;
    IF length(p_name) > 48 THEN
        RAISE EXCEPTION 'Register name too long: %. Maximum 48 characters', p_name;
    END IF;
    IF p_name !~ '^[a-z][a-z0-9_]*$' THEN
        RAISE EXCEPTION 'Invalid register name: %. Must start with a lowercase letter and contain only lowercase letters, digits, and underscores', p_name;
    END IF;
END;
$$;

CREATE OR REPLACE FUNCTION accum._validate_pg_type(p_type text)
RETURNS void
LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
    base_type text;
BEGIN
    -- Extract base type name (strip precision like "numeric(18,2)")
    base_type := lower(trim(regexp_replace(p_type, '\(.*\)', '')));
    IF base_type NOT IN (
        'int', 'int2', 'int4', 'int8', 'integer', 'smallint', 'bigint',
        'numeric', 'decimal', 'real', 'float', 'float4', 'float8',
        'double precision', 'money',
        'text', 'varchar', 'char', 'character varying', 'character',
        'boolean', 'bool',
        'date', 'timestamp', 'timestamptz', 'time', 'timetz',
        'interval',
        'uuid', 'jsonb', 'json'
    ) THEN
        RAISE EXCEPTION 'Unsupported PostgreSQL type: "%". Use standard types like int, text, numeric, etc.', p_type;
    END IF;
END;
$$;

CREATE OR REPLACE FUNCTION accum._validate_dimensions(p_dimensions jsonb)
RETURNS void
LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
    dim_key  text;
    dim_type text;
BEGIN
    IF p_dimensions IS NULL OR p_dimensions = '{}'::jsonb THEN
        RAISE EXCEPTION 'At least one dimension is required';
    END IF;
    IF jsonb_typeof(p_dimensions) != 'object' THEN
        RAISE EXCEPTION 'Dimensions must be a JSON object';
    END IF;
    FOR dim_key, dim_type IN SELECT * FROM jsonb_each_text(p_dimensions)
    LOOP
        IF dim_key !~ '^[a-z][a-z0-9_]*$' THEN
            RAISE EXCEPTION 'Invalid dimension name: %', dim_key;
        END IF;
        IF dim_type IS NULL OR dim_type = '' THEN
            RAISE EXCEPTION 'Dimension "%" must have a type', dim_key;
        END IF;
        -- Validate type against whitelist to prevent SQL injection
        PERFORM accum._validate_pg_type(dim_type);
    END LOOP;
END;
$$;

CREATE OR REPLACE FUNCTION accum._validate_resources(p_resources jsonb)
RETURNS void
LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
    res_key  text;
    res_type text;
    base_type text;
BEGIN
    IF p_resources IS NULL OR p_resources = '{}'::jsonb THEN
        RAISE EXCEPTION 'At least one resource is required';
    END IF;
    IF jsonb_typeof(p_resources) != 'object' THEN
        RAISE EXCEPTION 'Resources must be a JSON object';
    END IF;
    FOR res_key, res_type IN SELECT * FROM jsonb_each_text(p_resources)
    LOOP
        IF res_key !~ '^[a-z][a-z0-9_]*$' THEN
            RAISE EXCEPTION 'Invalid resource name: %', res_key;
        END IF;
        IF res_type IS NULL OR res_type = '' THEN
            RAISE EXCEPTION 'Resource "%" must have a type', res_key;
        END IF;
        -- Validate type against whitelist
        PERFORM accum._validate_pg_type(res_type);
        -- Resources must be numeric types
        base_type := lower(trim(regexp_replace(res_type, '\(.*\)', '')));
        IF base_type NOT IN (
            'int', 'int2', 'int4', 'int8', 'integer', 'smallint', 'bigint',
            'numeric', 'decimal', 'real', 'float', 'float4', 'float8',
            'double precision', 'money'
        ) THEN
            RAISE EXCEPTION 'Resource "%" must be a numeric type, got: "%"', res_key, res_type;
        END IF;
    END LOOP;
END;
$$;

-- ============================================================
-- REGISTRY CRUD FUNCTIONS
-- ============================================================

CREATE OR REPLACE FUNCTION accum._register_exists(p_name text)
RETURNS boolean
LANGUAGE sql STABLE AS $$
    SELECT EXISTS (SELECT 1 FROM accum._registers WHERE name = p_name);
$$;

CREATE OR REPLACE FUNCTION accum._register_get(p_name text)
RETURNS accum._registers
LANGUAGE sql STABLE AS $$
    SELECT * FROM accum._registers WHERE name = p_name;
$$;

CREATE OR REPLACE FUNCTION accum._register_put(
    p_name          text,
    p_kind          text    DEFAULT 'balance',
    p_dimensions    jsonb   DEFAULT NULL,
    p_resources     jsonb   DEFAULT NULL,
    p_totals_period text    DEFAULT 'day',
    p_partition_by  text    DEFAULT 'month',
    p_high_write    boolean DEFAULT false,
    p_recorder_type text    DEFAULT 'text'
) RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
    INSERT INTO accum._registers
        (name, kind, dimensions, resources, totals_period, partition_by, high_write, recorder_type)
    VALUES
        (p_name, p_kind, p_dimensions, p_resources, p_totals_period, p_partition_by, p_high_write, p_recorder_type)
    ON CONFLICT (name) DO UPDATE SET
        kind          = EXCLUDED.kind,
        dimensions    = EXCLUDED.dimensions,
        resources     = EXCLUDED.resources,
        totals_period = EXCLUDED.totals_period,
        partition_by  = EXCLUDED.partition_by,
        high_write    = EXCLUDED.high_write,
        recorder_type = EXCLUDED.recorder_type,
        updated_at    = now();
END;
$$;

CREATE OR REPLACE FUNCTION accum._register_delete(p_name text)
RETURNS boolean
LANGUAGE plpgsql AS $$
BEGIN
    DELETE FROM accum._registers WHERE name = p_name;
    RETURN FOUND;
END;
$$;

CREATE OR REPLACE FUNCTION accum._register_list()
RETURNS SETOF accum._registers
LANGUAGE sql STABLE AS $$
    SELECT * FROM accum._registers ORDER BY name;
$$;

-- ============================================================
-- HASH FUNCTION GENERATION
-- ============================================================

CREATE OR REPLACE FUNCTION accum._generate_hash_function(
    p_name       text,
    p_dimensions jsonb
) RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
    dim_key   text;
    dim_type  text;
    hash_args text := '';
    hash_body text := '';
    arg_idx   int  := 0;
BEGIN
    PERFORM accum._validate_name(p_name);
    PERFORM accum._validate_dimensions(p_dimensions);

    FOR dim_key, dim_type IN SELECT key, value FROM jsonb_each_text(p_dimensions) ORDER BY key
    LOOP
        IF arg_idx > 0 THEN
            hash_args := hash_args || ', ';
            hash_body := hash_body || ' || ''|'' || ';
        END IF;
        hash_args := hash_args || format('%I %s', 'p_' || dim_key, dim_type);
        hash_body := hash_body || format('coalesce(%I::text, ''__NULL__'')', 'p_' || dim_key);
        arg_idx := arg_idx + 1;
    END LOOP;

    EXECUTE format(
        'CREATE OR REPLACE FUNCTION accum.%I(%s) RETURNS bigint
         LANGUAGE sql IMMUTABLE AS $fn$
             SELECT hashtextextended(%s, 0)
         $fn$',
        '_hash_' || p_name,
        hash_args,
        hash_body
    );
END;
$$;

CREATE OR REPLACE FUNCTION accum._drop_hash_function(p_name text)
RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
    r record;
BEGIN
    FOR r IN
        SELECT p.oid::regprocedure::text AS fn
        FROM pg_proc p
        JOIN pg_namespace n ON p.pronamespace = n.oid
        WHERE n.nspname = 'accum'
          AND p.proname = '_hash_' || p_name
    LOOP
        BEGIN
            EXECUTE format('DROP FUNCTION %s CASCADE', r.fn);
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'Skipping drop of hash function %: %', r.fn, SQLERRM;
        END;
    END LOOP;
END;
$$;
