-- TAPDB Schema v0.1.0
-- Templated Abstract Polymorphic Database
-- PostgreSQL 13+ required

--------------------------------------------------------------------------------
-- SEQUENCES
--------------------------------------------------------------------------------

-- Core sequences (always required)
CREATE SEQUENCE IF NOT EXISTS generic_template_seq;
-- GX is the default instance prefix
CREATE SEQUENCE IF NOT EXISTS gx_instance_seq;
CREATE SEQUENCE IF NOT EXISTS generic_instance_lineage_seq;

-- Optional library sequences
CREATE SEQUENCE IF NOT EXISTS wx_instance_seq;   -- WX (workflow)
CREATE SEQUENCE IF NOT EXISTS wsx_instance_seq;  -- WSX (workflow_step)
CREATE SEQUENCE IF NOT EXISTS xx_instance_seq;   -- XX (action)
CREATE SEQUENCE IF NOT EXISTS ay_instance_seq;   -- AY (assay)

--------------------------------------------------------------------------------
-- TABLES
--------------------------------------------------------------------------------

-- Prefix configuration for identity tables whose EUID prefixes are env-configured
CREATE TABLE IF NOT EXISTS tapdb_identity_prefix_config (
    entity TEXT PRIMARY KEY,
    prefix TEXT NOT NULL,
    updated_dt TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Default identity prefixes required by triggers.
--
-- audit_log EUID generation is config-driven (see set_audit_log_euid()).
-- Ensure a sane default exists for fresh installs/tests; operators may override
-- this by updating tapdb_identity_prefix_config as needed.
INSERT INTO tapdb_identity_prefix_config (entity, prefix)
VALUES ('audit_log', 'AD')
ON CONFLICT (entity) DO NOTHING;

-- generic_template: Blueprint definitions
CREATE TABLE IF NOT EXISTS generic_template (
    -- Primary identification
    uid BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    euid TEXT UNIQUE NOT NULL,  -- Meridian EUID set by trigger (set_generic_template_euid)
    euid_prefix TEXT NOT NULL,
    euid_seq BIGINT NOT NULL,
    name TEXT NOT NULL,
    -- Tenant scoping (nullable for transition; will be enforced via RLS/policies later)
    tenant_id UUID,

    -- Type hierarchy (category/type/subtype/version)
    polymorphic_discriminator TEXT NOT NULL,
    category TEXT NOT NULL,
    type TEXT NOT NULL,
    subtype TEXT NOT NULL,
    version TEXT NOT NULL,

    CONSTRAINT unique_template_code UNIQUE (category, type, subtype, version),

    -- Instance configuration
    instance_prefix TEXT NOT NULL,
    instance_polymorphic_identity TEXT,

    -- Flexible data storage
    json_addl JSONB NOT NULL DEFAULT '{}'::jsonb,
    json_addl_schema JSONB,

    -- Status and lifecycle
    bstatus TEXT NOT NULL,
    is_singleton BOOLEAN NOT NULL DEFAULT TRUE,
    is_deleted BOOLEAN NOT NULL DEFAULT FALSE,

    -- Timestamps
    created_dt TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    modified_dt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT unique_generic_template_prefix_seq UNIQUE (euid_prefix, euid_seq)
);

-- generic_instance: Concrete objects created from templates
CREATE TABLE IF NOT EXISTS generic_instance (
    -- Primary identification
    uid BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    euid TEXT UNIQUE NOT NULL,
    euid_prefix TEXT NOT NULL,
    euid_seq BIGINT NOT NULL,
    name TEXT NOT NULL,
    -- Tenant scoping (nullable for transition; will be enforced via RLS/policies later)
    tenant_id UUID,

    -- Type hierarchy (copied from template: category/type/subtype/version)
    polymorphic_discriminator TEXT NOT NULL,
    category TEXT NOT NULL,
    type TEXT NOT NULL,
    subtype TEXT NOT NULL,
    version TEXT NOT NULL,

    -- Template reference
    template_uid BIGINT NOT NULL REFERENCES generic_template(uid),

    -- Flexible data storage
    json_addl JSONB NOT NULL DEFAULT '{}'::jsonb,

    -- Status and lifecycle
    bstatus TEXT NOT NULL,
    is_singleton BOOLEAN NOT NULL DEFAULT FALSE,
    is_deleted BOOLEAN NOT NULL DEFAULT FALSE,

    -- Timestamps
    created_dt TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    modified_dt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT unique_generic_instance_prefix_seq UNIQUE (euid_prefix, euid_seq)
);

-- generic_instance_lineage: Directed edges between instances
CREATE TABLE IF NOT EXISTS generic_instance_lineage (
    -- Primary identification
    uid BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    euid TEXT UNIQUE NOT NULL,  -- Meridian EUID set by trigger (set_generic_instance_lineage_euid)
    euid_prefix TEXT NOT NULL,
    euid_seq BIGINT NOT NULL,
    name TEXT NOT NULL,
    -- Tenant scoping (nullable for transition; will be enforced via RLS/policies later)
    tenant_id UUID,

    -- Type hierarchy (category/type/subtype/version)
    polymorphic_discriminator TEXT NOT NULL,
    category TEXT NOT NULL DEFAULT 'lineage',
    type TEXT NOT NULL DEFAULT 'lineage',
    subtype TEXT NOT NULL DEFAULT 'generic',
    version TEXT NOT NULL DEFAULT '1.0',

    -- Relationship definition
    parent_instance_uid BIGINT NOT NULL REFERENCES generic_instance(uid),
    child_instance_uid BIGINT NOT NULL REFERENCES generic_instance(uid),
    parent_type TEXT,
    child_type TEXT,
    relationship_type TEXT NOT NULL DEFAULT 'generic',

    -- Flexible data storage
    json_addl JSONB NOT NULL DEFAULT '{}'::jsonb,

    -- Status and lifecycle
    bstatus TEXT NOT NULL DEFAULT 'active',
    is_singleton BOOLEAN NOT NULL DEFAULT FALSE,
    is_deleted BOOLEAN NOT NULL DEFAULT FALSE,

    -- Timestamps
    created_dt TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    modified_dt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT unique_generic_instance_lineage_prefix_seq UNIQUE (euid_prefix, euid_seq)
);

-- audit_log: Change tracking
CREATE TABLE IF NOT EXISTS audit_log (
    uid BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    euid TEXT UNIQUE NOT NULL,
    euid_prefix TEXT NOT NULL,
    euid_seq BIGINT NOT NULL,
    -- Tenant scoping copied from source row (nullable for legacy rows)
    tenant_id UUID,
    rel_table_name TEXT NOT NULL,
    column_name TEXT,
    rel_table_uid_fk BIGINT NOT NULL,
    rel_table_euid_fk TEXT NOT NULL,
    old_value TEXT,
    new_value TEXT,
    changed_by TEXT,
    changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    operation_type TEXT CHECK (operation_type IN ('INSERT', 'UPDATE', 'DELETE')),
    json_addl JSONB,
    category TEXT,
    deleted_record_json JSONB,
    is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
    is_singleton BOOLEAN NOT NULL DEFAULT FALSE,

    CONSTRAINT unique_audit_log_prefix_seq UNIQUE (euid_prefix, euid_seq)
);

-- outbox_event: Transactional outbox for durable cross-service events
CREATE TABLE IF NOT EXISTS outbox_event (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    event_id UUID NOT NULL,
    tenant_id UUID,
    event_type TEXT NOT NULL,
    aggregate_euid TEXT,
    payload JSONB NOT NULL,
    destination TEXT NOT NULL,
    dedupe_key TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'delivering', 'delivered', 'failed')),
    attempt_count INT NOT NULL DEFAULT 0,
    next_attempt_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    last_error TEXT,
    created_dt TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    delivered_dt TIMESTAMP WITH TIME ZONE,

    CONSTRAINT unique_outbox_event_destination_dedupe_key UNIQUE (destination, dedupe_key)
);

-- _tapdb_migrations: migration tracking for schema evolution (not used for fresh installs)
CREATE TABLE IF NOT EXISTS _tapdb_migrations (
    filename TEXT PRIMARY KEY,
    applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

--------------------------------------------------------------------------------
-- HARDENING (idempotent-ish ALTERs for existing installs)
--------------------------------------------------------------------------------

DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1
        FROM pg_constraint
        WHERE conname = 'unique_template_code'
          AND conrelid = 'generic_template'::regclass
    ) THEN
        ALTER TABLE generic_template ADD CONSTRAINT unique_template_code
            UNIQUE (category, type, subtype, version);
    END IF;
END $$;

ALTER TABLE generic_instance ALTER COLUMN euid SET NOT NULL;

ALTER TABLE generic_template ALTER COLUMN json_addl SET DEFAULT '{}'::jsonb;
ALTER TABLE generic_instance ALTER COLUMN json_addl SET DEFAULT '{}'::jsonb;

ALTER TABLE generic_template ADD COLUMN IF NOT EXISTS tenant_id UUID;
ALTER TABLE generic_instance ADD COLUMN IF NOT EXISTS tenant_id UUID;
ALTER TABLE generic_instance_lineage ADD COLUMN IF NOT EXISTS tenant_id UUID;
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS tenant_id UUID;

--------------------------------------------------------------------------------
-- INDEXES
--------------------------------------------------------------------------------

-- generic_template indexes
CREATE INDEX IF NOT EXISTS idx_generic_template_polymorphic_discriminator ON generic_template(polymorphic_discriminator);
CREATE INDEX IF NOT EXISTS idx_generic_template_type ON generic_template(type);
CREATE INDEX IF NOT EXISTS idx_generic_template_is_deleted ON generic_template(is_deleted);

-- generic_instance indexes
CREATE UNIQUE INDEX IF NOT EXISTS idx_generic_instance_unique_singleton_key
    ON generic_instance (category, type, subtype, version)
    WHERE is_singleton = TRUE;
CREATE INDEX IF NOT EXISTS idx_generic_instance_polymorphic_discriminator ON generic_instance(polymorphic_discriminator);
CREATE INDEX IF NOT EXISTS idx_generic_instance_type ON generic_instance(type);
CREATE INDEX IF NOT EXISTS idx_generic_instance_euid ON generic_instance(euid);
CREATE INDEX IF NOT EXISTS idx_generic_instance_is_deleted ON generic_instance(is_deleted);
CREATE INDEX IF NOT EXISTS idx_generic_instance_template_uid ON generic_instance(template_uid);
CREATE INDEX IF NOT EXISTS idx_generic_instance_category ON generic_instance(category);
CREATE INDEX IF NOT EXISTS idx_generic_instance_subtype ON generic_instance(subtype);
CREATE INDEX IF NOT EXISTS idx_generic_instance_version ON generic_instance(version);
CREATE INDEX IF NOT EXISTS idx_generic_instance_mod_dt ON generic_instance(modified_dt);
CREATE INDEX IF NOT EXISTS idx_generic_instance_singleton ON generic_instance(is_singleton);
CREATE INDEX IF NOT EXISTS idx_generic_instance_json_addl_gin ON generic_instance USING GIN (json_addl);
CREATE INDEX IF NOT EXISTS idx_generic_instance_tenant_template_created_dt
    ON generic_instance (tenant_id, category, type, subtype, version, created_dt DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_generic_instance_system_user_login_identifier
    ON generic_instance (lower(COALESCE(json_addl->>'login_identifier', '')))
    WHERE is_deleted = FALSE
      AND polymorphic_discriminator = 'actor_instance'
      AND category = 'generic'
      AND type = 'actor'
      AND subtype = 'system_user';

-- generic_instance_lineage indexes
CREATE UNIQUE INDEX IF NOT EXISTS idx_lineage_unique_edge
    ON generic_instance_lineage (parent_instance_uid, child_instance_uid, relationship_type)
    WHERE is_deleted = FALSE;
CREATE INDEX IF NOT EXISTS idx_generic_instance_lineage_parent ON generic_instance_lineage(parent_instance_uid);
CREATE INDEX IF NOT EXISTS idx_generic_instance_lineage_child ON generic_instance_lineage(child_instance_uid);
CREATE INDEX IF NOT EXISTS idx_generic_instance_lineage_is_deleted ON generic_instance_lineage(is_deleted);

-- audit_log indexes
CREATE INDEX IF NOT EXISTS idx_audit_log_rel_table_name ON audit_log(rel_table_name);
CREATE INDEX IF NOT EXISTS idx_audit_log_rel_table_uid_fk ON audit_log(rel_table_uid_fk);
CREATE INDEX IF NOT EXISTS idx_audit_log_rel_table_euid_fk ON audit_log(rel_table_euid_fk);
CREATE INDEX IF NOT EXISTS idx_audit_log_is_deleted ON audit_log(is_deleted);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_changed_at ON audit_log(changed_at);
CREATE INDEX IF NOT EXISTS idx_audit_log_changed_by ON audit_log(changed_by);
CREATE INDEX IF NOT EXISTS idx_audit_log_json_addl_gin ON audit_log USING GIN (json_addl);

-- outbox_event indexes
CREATE INDEX IF NOT EXISTS idx_outbox_event_status_next_attempt_at
    ON outbox_event(status, next_attempt_at);
CREATE INDEX IF NOT EXISTS idx_outbox_event_tenant_created_dt
    ON outbox_event(tenant_id, created_dt DESC);

--------------------------------------------------------------------------------
-- FUNCTIONS
--------------------------------------------------------------------------------

-- Meridian EUID: Crockford Base32 encode (positive integer -> unpadded text)
CREATE OR REPLACE FUNCTION crockford_base32_encode(val BIGINT)
RETURNS TEXT LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    alphabet TEXT := '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
    result TEXT := '';
    remainder BIGINT;
BEGIN
    IF val <= 0 THEN
        RAISE EXCEPTION 'EUID body must be a positive integer, got %', val;
    END IF;
    WHILE val > 0 LOOP
        remainder := val % 32;
        result := substr(alphabet, (remainder + 1)::integer, 1) || result;
        val := val / 32;
    END LOOP;
    RETURN result;
END;
$$;

-- Meridian EUID: Crockford Base32 decode (unpadded text -> integer)
CREATE OR REPLACE FUNCTION crockford_base32_decode(val TEXT)
RETURNS BIGINT LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    alphabet TEXT := '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
    idx INT;
    out_val BIGINT := 0;
    ch CHAR;
BEGIN
    IF val IS NULL OR val = '' THEN
        RAISE EXCEPTION 'EUID body must be non-empty';
    END IF;

    FOR idx IN 1..length(val) LOOP
        ch := substr(val, idx, 1);
        IF position(ch IN alphabet) = 0 THEN
            RAISE EXCEPTION 'Invalid character "%" in EUID body', ch;
        END IF;
        out_val := out_val * 32 + (position(ch IN alphabet) - 1);
    END LOOP;

    IF out_val <= 0 THEN
        RAISE EXCEPTION 'Decoded EUID body must be a positive integer, got %', out_val;
    END IF;

    RETURN out_val;
END;
$$;

-- Validate Meridian prefix (2-3 uppercase letters, no I/L/O/U)
CREATE OR REPLACE FUNCTION tapdb_validate_meridian_prefix(prefix TEXT)
RETURNS TEXT LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    norm TEXT := upper(trim(prefix));
BEGIN
    IF norm !~ '^[A-HJ-KMNP-TV-Z]{2,3}$' THEN
        RAISE EXCEPTION
            'Invalid Meridian prefix "%" (must match ^[A-HJ-KMNP-TV-Z]{2,3}$)',
            prefix;
    END IF;
    RETURN norm;
END;
$$;

-- Validate sandbox prefix (single uppercase letter, no I/L/O/U, sandbox-only range)
CREATE OR REPLACE FUNCTION tapdb_validate_sandbox_prefix(prefix TEXT)
RETURNS TEXT LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    norm TEXT := upper(trim(prefix));
BEGIN
    IF norm !~ '^[HJ-KMNP-TV-Z]$' THEN
        RAISE EXCEPTION
            'Invalid sandbox prefix "%" (must match ^[HJ-KMNP-TV-Z]$)',
            prefix;
    END IF;
    RETURN norm;
END;
$$;

-- Resolve runtime sandbox prefix from Postgres session state.
-- Missing session state defaults to T. Explicit empty string disables prefixing.
CREATE OR REPLACE FUNCTION tapdb_current_sandbox_prefix()
RETURNS TEXT LANGUAGE plpgsql STABLE AS $$
DECLARE
    raw_prefix TEXT;
BEGIN
    raw_prefix := current_setting('session.current_sandbox_prefix', true);
    IF raw_prefix IS NULL THEN
        RETURN 'T';
    END IF;

    raw_prefix := trim(raw_prefix);
    IF raw_prefix = '' THEN
        RETURN NULL;
    END IF;

    RETURN tapdb_validate_sandbox_prefix(raw_prefix);
END;
$$;

-- Extract sandbox prefix from EUID string (e.g. T:GX-1AB -> T)
CREATE OR REPLACE FUNCTION meridian_euid_sandbox_prefix(euid TEXT)
RETURNS TEXT LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    colon_pos INT;
    prefix TEXT;
BEGIN
    colon_pos := position(':' IN euid);
    IF colon_pos = 0 THEN
        RETURN NULL;
    END IF;
    IF colon_pos <> 2 THEN
        RAISE EXCEPTION 'Invalid sandbox EUID format (bad prefix delimiter): %', euid;
    END IF;

    prefix := substr(euid, 1, colon_pos - 1);
    RETURN tapdb_validate_sandbox_prefix(prefix);
END;
$$;

-- Extract category prefix from EUID string (e.g. GX-1AB -> GX, T:GX-1AB -> GX)
CREATE OR REPLACE FUNCTION meridian_euid_prefix(euid TEXT)
RETURNS TEXT LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    colon_pos INT;
    dash_pos INT;
    prefix_start INT := 1;
    prefix TEXT;
BEGIN
    colon_pos := position(':' IN euid);
    IF colon_pos > 0 THEN
        prefix_start := colon_pos + 1;
    END IF;
    dash_pos := position('-' IN euid);
    IF dash_pos <= prefix_start THEN
        RAISE EXCEPTION 'Invalid EUID format (missing prefix): %', euid;
    END IF;
    prefix := substr(euid, prefix_start, dash_pos - prefix_start);
    RETURN tapdb_validate_meridian_prefix(prefix);
END;
$$;

-- Extract numeric sequence from EUID body (no checksum)
CREATE OR REPLACE FUNCTION meridian_euid_seq_from_euid(euid TEXT)
RETURNS BIGINT LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    dash_pos INT;
    body_plus_check TEXT;
    body TEXT;
BEGIN
    dash_pos := position('-' IN euid);
    IF dash_pos < 2 THEN
        RAISE EXCEPTION 'Invalid EUID format: %', euid;
    END IF;

    body_plus_check := substr(euid, dash_pos + 1);
    IF length(body_plus_check) < 2 THEN
        RAISE EXCEPTION 'Invalid EUID body/checksum segment: %', euid;
    END IF;

    body := substr(body_plus_check, 1, length(body_plus_check) - 1);
    RETURN crockford_base32_decode(body);
END;
$$;

-- Meridian EUID: Luhn-style MOD 32 check character (SPEC.md §7.5)
CREATE OR REPLACE FUNCTION meridian_luhn_mod32_check(payload TEXT)
RETURNS CHAR LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
    alphabet TEXT := '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
    i INT; ch CHAR; v INT; factor INT; p INT;
    total INT := 0; payload_len INT; check_value INT;
BEGIN
    payload_len := length(payload);
    FOR i IN REVERSE payload_len..1 LOOP
        ch := substr(payload, i, 1);
        v := position(ch IN alphabet) - 1;
        IF v < 0 THEN
            RAISE EXCEPTION 'Invalid character "%" in EUID payload', ch;
        END IF;
        factor := CASE WHEN (payload_len - i) % 2 = 0 THEN 2 ELSE 1 END;
        p := v * factor;
        total := total + (p / 32) + (p % 32);
    END LOOP;
    check_value := (32 - (total % 32)) % 32;
    RETURN substr(alphabet, check_value + 1, 1);
END;
$$;

-- Meridian EUID: Generate full EUID string (PREFIX-BODYCHECK or T:PREFIX-BODYCHECK)
CREATE OR REPLACE FUNCTION meridian_generate_euid(prefix TEXT, seq_val BIGINT, sandbox_prefix TEXT)
RETURNS TEXT LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
    normalized_prefix TEXT;
    normalized_sandbox_prefix TEXT;
    body TEXT;
    payload TEXT;
    check_char CHAR;
BEGIN
    normalized_prefix := tapdb_validate_meridian_prefix(prefix);
    normalized_sandbox_prefix := CASE
        WHEN sandbox_prefix IS NULL OR trim(sandbox_prefix) = '' THEN NULL
        ELSE tapdb_validate_sandbox_prefix(sandbox_prefix)
    END;

    body := crockford_base32_encode(seq_val);
    payload := COALESCE(normalized_sandbox_prefix, '') || normalized_prefix || body;
    check_char := meridian_luhn_mod32_check(payload);

    IF normalized_sandbox_prefix IS NULL THEN
        RETURN normalized_prefix || '-' || body || check_char;
    END IF;
    RETURN normalized_sandbox_prefix || ':' || normalized_prefix || '-' || body || check_char;
END;
$$;

CREATE OR REPLACE FUNCTION meridian_generate_euid(prefix TEXT, seq_val BIGINT)
RETURNS TEXT LANGUAGE plpgsql STABLE STRICT AS $$
BEGIN
    RETURN meridian_generate_euid(prefix, seq_val, tapdb_current_sandbox_prefix());
END;
$$;

-- Resolve configured EUID prefix for identity table
CREATE OR REPLACE FUNCTION tapdb_get_identity_prefix(entity_name TEXT)
RETURNS TEXT LANGUAGE plpgsql STABLE STRICT AS $$
DECLARE
    prefix TEXT;
BEGIN
    SELECT p.prefix INTO prefix
    FROM tapdb_identity_prefix_config p
    WHERE p.entity = entity_name;

    IF prefix IS NULL THEN
        RAISE EXCEPTION
            'Missing EUID prefix configuration for entity "%" in tapdb_identity_prefix_config',
            entity_name;
    END IF;

    RETURN tapdb_validate_meridian_prefix(prefix);
END;
$$;

-- EUID auto-generation for generic_template (Meridian-conformant)
CREATE OR REPLACE FUNCTION set_generic_template_euid()
RETURNS TRIGGER AS $$
DECLARE
    prefix TEXT;
    sandbox_prefix TEXT;
    seq_val BIGINT;
BEGIN
    IF NEW.euid IS NULL OR NEW.euid = '' THEN
        prefix := 'GT';
        seq_val := nextval('generic_template_seq');
        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
        NEW.euid := meridian_generate_euid(prefix, seq_val);
    ELSE
        prefix := COALESCE(NEW.euid_prefix, meridian_euid_prefix(NEW.euid));
        seq_val := COALESCE(NEW.euid_seq, meridian_euid_seq_from_euid(NEW.euid));
        sandbox_prefix := meridian_euid_sandbox_prefix(NEW.euid);
        prefix := tapdb_validate_meridian_prefix(prefix);
        IF NEW.euid <> meridian_generate_euid(prefix, seq_val, sandbox_prefix) THEN
            RAISE EXCEPTION 'Provided EUID does not match provided/generated prefix+seq: %', NEW.euid;
        END IF;
        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- EUID auto-generation for generic_instance (Meridian-conformant)
CREATE OR REPLACE FUNCTION set_generic_instance_euid()
RETURNS TRIGGER AS $$
DECLARE
    prefix TEXT;
    sandbox_prefix TEXT;
    seq_val BIGINT;
    seq_name TEXT;
BEGIN
    IF NEW.euid IS NULL OR NEW.euid = '' THEN
        -- Get prefix from template
        SELECT instance_prefix INTO prefix FROM generic_template WHERE uid = NEW.template_uid;

        -- Default prefix if template not found or no prefix set
        IF prefix IS NULL THEN
            prefix := 'GX';
        END IF;
        prefix := tapdb_validate_meridian_prefix(prefix);

        -- Dynamic sequence resolution (allows new prefixes without trigger changes)
        seq_name := lower(prefix) || '_instance_seq';

        BEGIN
            EXECUTE format('SELECT nextval(%L)', seq_name) INTO seq_val;
        EXCEPTION WHEN undefined_table OR undefined_object THEN
            RAISE EXCEPTION
                'Missing EUID sequence % for instance_prefix %. Create and initialize it before inserting instances.',
                seq_name, prefix;
        END;

        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
        NEW.euid := meridian_generate_euid(prefix, seq_val);
    ELSE
        prefix := COALESCE(NEW.euid_prefix, meridian_euid_prefix(NEW.euid));
        seq_val := COALESCE(NEW.euid_seq, meridian_euid_seq_from_euid(NEW.euid));
        sandbox_prefix := meridian_euid_sandbox_prefix(NEW.euid);
        prefix := tapdb_validate_meridian_prefix(prefix);
        IF NEW.euid <> meridian_generate_euid(prefix, seq_val, sandbox_prefix) THEN
            RAISE EXCEPTION 'Provided EUID does not match provided/generated prefix+seq: %', NEW.euid;
        END IF;
        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- EUID auto-generation for generic_instance_lineage (Meridian-conformant)
CREATE OR REPLACE FUNCTION set_generic_instance_lineage_euid()
RETURNS TRIGGER AS $$
DECLARE
    prefix TEXT;
    sandbox_prefix TEXT;
    seq_val BIGINT;
BEGIN
    IF NEW.euid IS NULL OR NEW.euid = '' THEN
        prefix := 'GN';
        seq_val := nextval('generic_instance_lineage_seq');
        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
        NEW.euid := meridian_generate_euid(prefix, seq_val);
    ELSE
        prefix := COALESCE(NEW.euid_prefix, meridian_euid_prefix(NEW.euid));
        seq_val := COALESCE(NEW.euid_seq, meridian_euid_seq_from_euid(NEW.euid));
        sandbox_prefix := meridian_euid_sandbox_prefix(NEW.euid);
        prefix := tapdb_validate_meridian_prefix(prefix);
        IF NEW.euid <> meridian_generate_euid(prefix, seq_val, sandbox_prefix) THEN
            RAISE EXCEPTION 'Provided EUID does not match provided/generated prefix+seq: %', NEW.euid;
        END IF;
        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- EUID auto-generation for audit_log (config-driven prefix)
CREATE OR REPLACE FUNCTION set_audit_log_euid()
RETURNS TRIGGER AS $$
DECLARE
    prefix TEXT;
    sandbox_prefix TEXT;
    seq_val BIGINT;
    seq_name TEXT;
BEGIN
    IF NEW.euid IS NULL OR NEW.euid = '' THEN
        prefix := tapdb_get_identity_prefix('audit_log');
        seq_name := lower(prefix) || '_audit_seq';
        EXECUTE format('CREATE SEQUENCE IF NOT EXISTS %I', seq_name);
        EXECUTE format('SELECT nextval(%L)', seq_name) INTO seq_val;

        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
        NEW.euid := meridian_generate_euid(prefix, seq_val);
    ELSE
        prefix := COALESCE(NEW.euid_prefix, meridian_euid_prefix(NEW.euid));
        seq_val := COALESCE(NEW.euid_seq, meridian_euid_seq_from_euid(NEW.euid));
        sandbox_prefix := meridian_euid_sandbox_prefix(NEW.euid);
        prefix := tapdb_validate_meridian_prefix(prefix);
        IF NEW.euid <> meridian_generate_euid(prefix, seq_val, sandbox_prefix) THEN
            RAISE EXCEPTION 'Provided EUID does not match provided/generated prefix+seq: %', NEW.euid;
        END IF;
        NEW.euid_prefix := prefix;
        NEW.euid_seq := seq_val;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Soft delete function (prevents actual deletion, sets is_deleted flag)
CREATE OR REPLACE FUNCTION soft_delete_row()
RETURNS TRIGGER AS $$
DECLARE
    app_username TEXT;
BEGIN
    -- Get current user for audit
    BEGIN
        app_username := current_setting('session.current_username', true);
    EXCEPTION WHEN OTHERS THEN
        app_username := current_user;
    END;

    -- Soft delete only the row in the triggering table (dynamic SQL)
    EXECUTE format('UPDATE %I SET is_deleted = TRUE WHERE uid = $1', TG_TABLE_NAME)
    USING OLD.uid;

    -- Record deletion in audit log with full record snapshot
    INSERT INTO audit_log (
        rel_table_name, rel_table_uid_fk, rel_table_euid_fk,
        tenant_id, changed_by, operation_type, old_value
    ) VALUES (
        TG_TABLE_NAME, OLD.uid, OLD.euid,
        OLD.tenant_id, app_username, 'DELETE', row_to_json(OLD)::TEXT
    );

    RETURN NULL;  -- Prevent actual deletion
END;
$$ LANGUAGE plpgsql;

-- Record UPDATE operations in audit log
CREATE OR REPLACE FUNCTION record_update()
RETURNS TRIGGER AS $$
DECLARE
    r RECORD;
    column_name TEXT;
    old_value TEXT;
    new_value TEXT;
    app_username TEXT;
BEGIN
    BEGIN
        app_username := current_setting('session.current_username', true);
    EXCEPTION WHEN OTHERS THEN
        app_username := current_user;
    END;

    FOR r IN SELECT * FROM json_each_text(row_to_json(NEW)) LOOP
        column_name := r.key;
        new_value := r.value;
        EXECUTE format('SELECT ($1).%I', column_name) USING OLD INTO old_value;

        IF old_value IS DISTINCT FROM new_value THEN
            INSERT INTO audit_log (rel_table_name, column_name, old_value, new_value,
                                   changed_by, rel_table_uid_fk, rel_table_euid_fk,
                                   tenant_id, operation_type)
            VALUES (TG_TABLE_NAME, column_name, old_value, new_value,
                    app_username, NEW.uid, NEW.euid,
                    NEW.tenant_id, TG_OP);
        END IF;
    END LOOP;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Record INSERT operations in audit log
CREATE OR REPLACE FUNCTION record_insert()
RETURNS TRIGGER AS $$
DECLARE
    app_username TEXT;
BEGIN
    BEGIN
        app_username := current_setting('session.current_username', true);
    EXCEPTION WHEN OTHERS THEN
        app_username := current_user;
    END;

    INSERT INTO audit_log (rel_table_name, rel_table_uid_fk, rel_table_euid_fk,
                           tenant_id, changed_by, operation_type)
    VALUES (TG_TABLE_NAME, NEW.uid, NEW.euid, NEW.tenant_id, app_username, 'INSERT');

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Auto-update modified_dt timestamp
CREATE OR REPLACE FUNCTION update_modified_dt()
RETURNS TRIGGER AS $$
BEGIN
    NEW.modified_dt = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

--------------------------------------------------------------------------------
-- TRIGGERS
--------------------------------------------------------------------------------

-- EUID trigger for generic_template (Meridian-conformant)
DROP TRIGGER IF EXISTS trigger_set_generic_template_euid ON generic_template;
CREATE TRIGGER trigger_set_generic_template_euid
    BEFORE INSERT ON generic_template
    FOR EACH ROW EXECUTE FUNCTION set_generic_template_euid();

-- EUID trigger for generic_instance (Meridian-conformant)
DROP TRIGGER IF EXISTS trigger_set_generic_instance_euid ON generic_instance;
CREATE TRIGGER trigger_set_generic_instance_euid
    BEFORE INSERT ON generic_instance
    FOR EACH ROW EXECUTE FUNCTION set_generic_instance_euid();

-- EUID trigger for generic_instance_lineage (Meridian-conformant)
DROP TRIGGER IF EXISTS trigger_set_generic_instance_lineage_euid ON generic_instance_lineage;
CREATE TRIGGER trigger_set_generic_instance_lineage_euid
    BEFORE INSERT ON generic_instance_lineage
    FOR EACH ROW EXECUTE FUNCTION set_generic_instance_lineage_euid();

-- EUID trigger for audit_log (config-driven)
DROP TRIGGER IF EXISTS trigger_set_audit_log_euid ON audit_log;
CREATE TRIGGER trigger_set_audit_log_euid
    BEFORE INSERT ON audit_log
    FOR EACH ROW EXECUTE FUNCTION set_audit_log_euid();

-- Soft delete triggers (BEFORE DELETE)
DROP TRIGGER IF EXISTS soft_delete_generic_template ON generic_template;
CREATE TRIGGER soft_delete_generic_template
    BEFORE DELETE ON generic_template
    FOR EACH ROW EXECUTE FUNCTION soft_delete_row();

DROP TRIGGER IF EXISTS soft_delete_generic_instance ON generic_instance;
CREATE TRIGGER soft_delete_generic_instance
    BEFORE DELETE ON generic_instance
    FOR EACH ROW EXECUTE FUNCTION soft_delete_row();

DROP TRIGGER IF EXISTS soft_delete_generic_instance_lineage ON generic_instance_lineage;
CREATE TRIGGER soft_delete_generic_instance_lineage
    BEFORE DELETE ON generic_instance_lineage
    FOR EACH ROW EXECUTE FUNCTION soft_delete_row();

-- Audit triggers (AFTER INSERT)
DROP TRIGGER IF EXISTS audit_insert_generic_template ON generic_template;
CREATE TRIGGER audit_insert_generic_template
    AFTER INSERT ON generic_template
    FOR EACH ROW EXECUTE FUNCTION record_insert();

DROP TRIGGER IF EXISTS audit_insert_generic_instance ON generic_instance;
CREATE TRIGGER audit_insert_generic_instance
    AFTER INSERT ON generic_instance
    FOR EACH ROW EXECUTE FUNCTION record_insert();

DROP TRIGGER IF EXISTS audit_insert_generic_instance_lineage ON generic_instance_lineage;
CREATE TRIGGER audit_insert_generic_instance_lineage
    AFTER INSERT ON generic_instance_lineage
    FOR EACH ROW EXECUTE FUNCTION record_insert();

-- Audit triggers (AFTER UPDATE)
DROP TRIGGER IF EXISTS audit_update_generic_template ON generic_template;
CREATE TRIGGER audit_update_generic_template
    AFTER UPDATE ON generic_template
    FOR EACH ROW EXECUTE FUNCTION record_update();

DROP TRIGGER IF EXISTS audit_update_generic_instance ON generic_instance;
CREATE TRIGGER audit_update_generic_instance
    AFTER UPDATE ON generic_instance
    FOR EACH ROW EXECUTE FUNCTION record_update();

DROP TRIGGER IF EXISTS audit_update_generic_instance_lineage ON generic_instance_lineage;
CREATE TRIGGER audit_update_generic_instance_lineage
    AFTER UPDATE ON generic_instance_lineage
    FOR EACH ROW EXECUTE FUNCTION record_update();

-- Modified timestamp triggers (BEFORE UPDATE)
DROP TRIGGER IF EXISTS update_modified_dt_generic_template ON generic_template;
CREATE TRIGGER update_modified_dt_generic_template
    BEFORE UPDATE ON generic_template
    FOR EACH ROW EXECUTE FUNCTION update_modified_dt();

DROP TRIGGER IF EXISTS update_modified_dt_generic_instance ON generic_instance;
CREATE TRIGGER update_modified_dt_generic_instance
    BEFORE UPDATE ON generic_instance
    FOR EACH ROW EXECUTE FUNCTION update_modified_dt();

DROP TRIGGER IF EXISTS update_modified_dt_generic_instance_lineage ON generic_instance_lineage;
CREATE TRIGGER update_modified_dt_generic_instance_lineage
    BEFORE UPDATE ON generic_instance_lineage
    FOR EACH ROW EXECUTE FUNCTION update_modified_dt();

--------------------------------------------------------------------------------
-- END OF SCHEMA
--------------------------------------------------------------------------------
