-- Migration: Event System and Actions
-- Description: Creates trigger, sensor, event, enforcement, and action tables
--              with runtime version constraint support. Includes webhook key
--              generation function used by webhook management functions in 000007.
--
--              NOTE: The event and enforcement tables are converted to TimescaleDB
--              hypertables in migration 000009. Hypertables cannot be the target of
--              FK constraints, so enforcement.event is a plain BIGINT with no FK.
--              FKs *from* hypertables to regular tables (e.g., event.trigger → trigger,
--              enforcement.rule → rule) are supported by TimescaleDB 2.x and are kept.
-- Version: 20250101000004

-- ============================================================================
-- WEBHOOK KEY GENERATION
-- ============================================================================

-- Generates a unique webhook key in the format: wh_<32 random hex chars>
-- Used by enable_trigger_webhook() and regenerate_trigger_webhook_key() in 000007.
CREATE OR REPLACE FUNCTION generate_webhook_key()
RETURNS VARCHAR(64) AS $$
BEGIN
    RETURN 'wh_' || encode(gen_random_bytes(16), 'hex');
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION generate_webhook_key() IS 'Generates a unique webhook key (format: wh_<32 hex chars>) for trigger webhook authentication';

-- ============================================================================
-- TRIGGER TABLE
-- ============================================================================

CREATE TABLE trigger (
    id BIGSERIAL PRIMARY KEY,
    ref TEXT NOT NULL UNIQUE,
    pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
    pack_ref TEXT,
    label TEXT NOT NULL,
    description TEXT,
    enabled BOOLEAN NOT NULL DEFAULT TRUE,
    is_adhoc BOOLEAN DEFAULT false NOT NULL,
    param_schema JSONB,
    out_schema JSONB,
    webhook_enabled BOOLEAN NOT NULL DEFAULT FALSE,
    webhook_key VARCHAR(64) UNIQUE,
    webhook_config JSONB DEFAULT '{}'::jsonb,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT trigger_ref_lowercase CHECK (ref = LOWER(ref)),
    CONSTRAINT trigger_ref_format CHECK (ref ~ '^[^.]+\.[^.]+$')
);

-- Indexes
CREATE INDEX idx_trigger_ref ON trigger(ref);
CREATE INDEX idx_trigger_pack ON trigger(pack);
CREATE INDEX idx_trigger_enabled ON trigger(enabled) WHERE enabled = TRUE;
CREATE INDEX idx_trigger_created ON trigger(created DESC);
CREATE INDEX idx_trigger_pack_enabled ON trigger(pack, enabled);
CREATE INDEX idx_trigger_webhook_key ON trigger(webhook_key) WHERE webhook_key IS NOT NULL;
CREATE INDEX idx_trigger_enabled_created ON trigger(enabled, created DESC) WHERE enabled = TRUE;

-- Trigger
CREATE TRIGGER update_trigger_updated
    BEFORE UPDATE ON trigger
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_column();

-- Comments
COMMENT ON TABLE trigger IS 'Trigger definitions that can activate rules';
COMMENT ON COLUMN trigger.ref IS 'Unique trigger reference (format: pack.name)';
COMMENT ON COLUMN trigger.label IS 'Human-readable trigger name';
COMMENT ON COLUMN trigger.enabled IS 'Whether this trigger is active';
COMMENT ON COLUMN trigger.param_schema IS 'JSON schema defining the expected configuration parameters when this trigger is used';
COMMENT ON COLUMN trigger.out_schema IS 'JSON schema defining the structure of event payloads generated by this trigger';

-- ============================================================================


-- ============================================================================
-- SENSOR TABLE
-- ============================================================================

CREATE TABLE sensor (
    id BIGSERIAL PRIMARY KEY,
    ref TEXT NOT NULL UNIQUE,
    pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
    pack_ref TEXT,
    label TEXT NOT NULL,
    description TEXT,
    entrypoint TEXT NOT NULL,
    runtime BIGINT NOT NULL REFERENCES runtime(id) ON DELETE CASCADE,
    runtime_ref TEXT NOT NULL,
    trigger BIGINT NOT NULL REFERENCES trigger(id) ON DELETE CASCADE,
    trigger_ref TEXT NOT NULL,
    enabled BOOLEAN NOT NULL,
    is_adhoc BOOLEAN NOT NULL DEFAULT FALSE,
    param_schema JSONB,
    config JSONB,
    runtime_version_constraint TEXT,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT sensor_ref_lowercase CHECK (ref = LOWER(ref)),
    CONSTRAINT sensor_ref_format CHECK (ref ~ '^[^.]+\.[^.]+$')
);

-- Indexes
CREATE INDEX idx_sensor_ref ON sensor(ref);
CREATE INDEX idx_sensor_pack ON sensor(pack);
CREATE INDEX idx_sensor_runtime ON sensor(runtime);
CREATE INDEX idx_sensor_trigger ON sensor(trigger);
CREATE INDEX idx_sensor_enabled ON sensor(enabled) WHERE enabled = TRUE;
CREATE INDEX idx_sensor_is_adhoc ON sensor(is_adhoc) WHERE is_adhoc = true;
CREATE INDEX idx_sensor_created ON sensor(created DESC);

-- Trigger
CREATE TRIGGER update_sensor_updated
    BEFORE UPDATE ON sensor
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_column();

-- Comments
COMMENT ON TABLE sensor IS 'Sensors monitor for events and create trigger instances';
COMMENT ON COLUMN sensor.ref IS 'Unique sensor reference (format: pack.name)';
COMMENT ON COLUMN sensor.label IS 'Human-readable sensor name';
COMMENT ON COLUMN sensor.entrypoint IS 'Script or command to execute';
COMMENT ON COLUMN sensor.runtime IS 'Runtime environment for execution';
COMMENT ON COLUMN sensor.trigger IS 'Trigger type this sensor creates events for';
COMMENT ON COLUMN sensor.enabled IS 'Whether this sensor is active';
COMMENT ON COLUMN sensor.is_adhoc IS 'True if sensor was manually created (ad-hoc), false if installed from pack';
COMMENT ON COLUMN sensor.runtime_version_constraint IS 'Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0"). NULL means any version.';

-- ============================================================================
-- EVENT TABLE
-- ============================================================================

CREATE TABLE event (
    id BIGSERIAL PRIMARY KEY,
    trigger BIGINT REFERENCES trigger(id) ON DELETE SET NULL,
    trigger_ref TEXT NOT NULL,
    config JSONB,
    payload JSONB,
    source BIGINT REFERENCES sensor(id) ON DELETE SET NULL,
    source_ref TEXT,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    rule BIGINT,
    rule_ref TEXT
);

-- Indexes
CREATE INDEX idx_event_trigger ON event(trigger);
CREATE INDEX idx_event_trigger_ref ON event(trigger_ref);
CREATE INDEX idx_event_source ON event(source);
CREATE INDEX idx_event_created ON event(created DESC);
CREATE INDEX idx_event_trigger_created ON event(trigger, created DESC);
CREATE INDEX idx_event_trigger_ref_created ON event(trigger_ref, created DESC);
CREATE INDEX idx_event_source_created ON event(source, created DESC);
CREATE INDEX idx_event_payload_gin ON event USING GIN (payload);

-- Comments
COMMENT ON TABLE event IS 'Events are instances of triggers firing';
COMMENT ON COLUMN event.trigger IS 'Trigger that fired (may be null if trigger deleted)';
COMMENT ON COLUMN event.trigger_ref IS 'Trigger reference (preserved even if trigger deleted)';
COMMENT ON COLUMN event.config IS 'Snapshot of trigger/sensor configuration at event time';
COMMENT ON COLUMN event.payload IS 'Event data payload';
COMMENT ON COLUMN event.source IS 'Sensor that generated this event';

-- ============================================================================
-- ENFORCEMENT TABLE
-- ============================================================================

CREATE TABLE enforcement (
    id BIGSERIAL PRIMARY KEY,
    rule BIGINT, -- Forward reference to rule table, will add constraint after rule is created
    rule_ref TEXT NOT NULL,
    trigger_ref TEXT NOT NULL,
    config JSONB,
    event BIGINT,           -- references event(id); no FK because event becomes a hypertable
    status enforcement_status_enum NOT NULL DEFAULT 'created',
    payload JSONB NOT NULL,
    condition enforcement_condition_enum NOT NULL DEFAULT 'all',
    conditions JSONB NOT NULL DEFAULT '[]'::jsonb,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    resolved_at TIMESTAMPTZ,

    -- Constraints
    CONSTRAINT enforcement_condition_check CHECK (condition IN ('any', 'all'))
);

-- Indexes
CREATE INDEX idx_enforcement_rule ON enforcement(rule);
CREATE INDEX idx_enforcement_rule_ref ON enforcement(rule_ref);
CREATE INDEX idx_enforcement_trigger_ref ON enforcement(trigger_ref);
CREATE INDEX idx_enforcement_event ON enforcement(event);
CREATE INDEX idx_enforcement_status ON enforcement(status);
CREATE INDEX idx_enforcement_created ON enforcement(created DESC);
CREATE INDEX idx_enforcement_status_created ON enforcement(status, created DESC);
CREATE INDEX idx_enforcement_rule_status ON enforcement(rule, status);
CREATE INDEX idx_enforcement_event_status ON enforcement(event, status);
CREATE INDEX idx_enforcement_payload_gin ON enforcement USING GIN (payload);
CREATE INDEX idx_enforcement_conditions_gin ON enforcement USING GIN (conditions);
CREATE UNIQUE INDEX uq_enforcement_rule_event
    ON enforcement (rule, event)
    WHERE rule IS NOT NULL AND event IS NOT NULL;

-- Comments
COMMENT ON TABLE enforcement IS 'Enforcements represent rule triggering by events';
COMMENT ON COLUMN enforcement.rule IS 'Rule being enforced (may be null if rule deleted)';
COMMENT ON COLUMN enforcement.rule_ref IS 'Rule reference (preserved even if rule deleted)';
COMMENT ON COLUMN enforcement.event IS 'Event that triggered this enforcement (no FK — event is a hypertable)';
COMMENT ON COLUMN enforcement.status IS 'Processing status (created → processed or disabled)';
COMMENT ON COLUMN enforcement.resolved_at IS 'Timestamp when the enforcement was resolved (status changed from created to processed/disabled). NULL while status is created.';
COMMENT ON COLUMN enforcement.payload IS 'Event payload for rule evaluation';
COMMENT ON COLUMN enforcement.condition IS 'Logical operator for conditions (any=OR, all=AND)';
COMMENT ON COLUMN enforcement.conditions IS 'Condition expressions to evaluate';

-- ============================================================================
-- ACTION TABLE
-- ============================================================================

CREATE TABLE action (
    id BIGSERIAL PRIMARY KEY,
    ref TEXT NOT NULL UNIQUE,
    pack BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE,
    pack_ref TEXT NOT NULL,
    label TEXT NOT NULL,
    description TEXT,
    entrypoint TEXT NOT NULL,
    runtime BIGINT REFERENCES runtime(id),
    param_schema JSONB,
    out_schema JSONB,
    parameter_delivery TEXT NOT NULL DEFAULT 'stdin' CHECK (parameter_delivery IN ('stdin', 'file')),
    parameter_format TEXT NOT NULL DEFAULT 'json' CHECK (parameter_format IN ('dotenv', 'json', 'yaml')),
    output_format TEXT NOT NULL DEFAULT 'text' CHECK (output_format IN ('text', 'json', 'yaml', 'jsonl')),
    is_adhoc BOOLEAN NOT NULL DEFAULT FALSE,
    timeout_seconds INTEGER,
    max_retries INTEGER DEFAULT 0,
    runtime_version_constraint TEXT,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT action_ref_lowercase CHECK (ref = LOWER(ref)),
    CONSTRAINT action_ref_format CHECK (ref ~ '^[^.]+\.[^.]+$')
);

-- Indexes
CREATE INDEX idx_action_ref ON action(ref);
CREATE INDEX idx_action_pack ON action(pack);
CREATE INDEX idx_action_runtime ON action(runtime);
CREATE INDEX idx_action_parameter_delivery ON action(parameter_delivery);
CREATE INDEX idx_action_parameter_format ON action(parameter_format);
CREATE INDEX idx_action_output_format ON action(output_format);
CREATE INDEX idx_action_is_adhoc ON action(is_adhoc) WHERE is_adhoc = true;
CREATE INDEX idx_action_created ON action(created DESC);

-- Trigger
CREATE TRIGGER update_action_updated
    BEFORE UPDATE ON action
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_column();

-- Comments
COMMENT ON TABLE action IS 'Actions are executable tasks that can be triggered';
COMMENT ON COLUMN action.ref IS 'Unique action reference (format: pack.name)';
COMMENT ON COLUMN action.pack IS 'Pack this action belongs to';
COMMENT ON COLUMN action.label IS 'Human-readable action name';
COMMENT ON COLUMN action.entrypoint IS 'Script or command to execute';
COMMENT ON COLUMN action.runtime IS 'Runtime environment for execution';
COMMENT ON COLUMN action.param_schema IS 'JSON schema for action parameters';
COMMENT ON COLUMN action.out_schema IS 'JSON schema for action output';
COMMENT ON COLUMN action.parameter_delivery IS 'How parameters are delivered: stdin (standard input - secure), file (temporary file - secure for large payloads). Environment variables are set separately via execution.env_vars.';
COMMENT ON COLUMN action.parameter_format IS 'Parameter serialization format: json (JSON object - default), dotenv (KEY=''VALUE''), yaml (YAML format)';
COMMENT ON COLUMN action.output_format IS 'Output parsing format: text (no parsing - raw stdout), json (parse stdout as JSON), yaml (parse stdout as YAML), jsonl (parse each line as JSON, collect into array)';
COMMENT ON COLUMN action.is_adhoc IS 'True if action was manually created (ad-hoc), false if installed from pack';
COMMENT ON COLUMN action.timeout_seconds IS 'Worker queue TTL override in seconds. If NULL, uses global worker_queue_ttl_ms config. Allows per-action timeout tuning.';
COMMENT ON COLUMN action.max_retries IS 'Maximum number of automatic retry attempts for failed executions. 0 = no retries (default).';
COMMENT ON COLUMN action.runtime_version_constraint IS 'Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0"). NULL means any version.';

-- ============================================================================

-- Add foreign key constraint for policy table
ALTER TABLE policy
    ADD CONSTRAINT policy_action_fkey
    FOREIGN KEY (action) REFERENCES action(id) ON DELETE CASCADE;

-- Note: Foreign key constraints for key table (key_owner_action_fkey, key_owner_sensor_fkey)
-- will be added in migration 000007_supporting_systems.sql after the key table is created

-- Note: Rule table will be created in migration 000005 after execution table exists
-- Note: Foreign key constraints for enforcement.rule and event.rule will be added there
