/**
 * WebhookDelivery -- HTTP POST delivery for EventBus events.
 *
 * Forwards governance events to external SIEM/webhook endpoints
 * with retry logic, HMAC signing, and dead-letter queue.
 *
 * Supports:
 *   - Splunk HEC, Datadog, Elastic SIEM, generic webhook
 *   - HMAC-SHA256 request signing
 *   - Exponential backoff retry (3 attempts)
 *   - Dead-letter queue for failed deliveries
 *   - Event filtering by type
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as http from 'http';
import * as crypto from 'crypto';
import { createLogger } from '../util/log.js';
const log = createLogger('webhook');
export class WebhookDelivery {
    config;
    dlqFile;
    statsDelivered = 0;
    statsFailed = 0;
    constructor(config) {
        this.config = {
            maxRetries: 3,
            retryDelayMs: 1000,
            timeoutMs: 10000,
            allowInsecure: false,
            ...config,
        };
        if (!fs.existsSync(config.dlqDir)) {
            fs.mkdirSync(config.dlqDir, { recursive: true });
        }
        this.dlqFile = path.join(config.dlqDir, 'webhook-dlq.jsonl');
    }
    /**
     * Deliver an event to all matching endpoints.
     */
    async deliver(event) {
        const results = [];
        for (const endpoint of this.config.endpoints) {
            if (endpoint.enabled === false)
                continue;
            // Filter by event type
            if (endpoint.eventTypes && endpoint.eventTypes.length > 0) {
                if (!endpoint.eventTypes.includes(event.type))
                    continue;
            }
            // Validate URL security
            if (!this.config.allowInsecure && !endpoint.url.startsWith('https://')) {
                results.push({
                    endpoint: endpoint.name,
                    success: false,
                    attempts: 0,
                    error: 'HTTPS required (set allowInsecure for testing)',
                });
                continue;
            }
            const result = await this.deliverToEndpoint(event, endpoint);
            results.push(result);
        }
        return results;
    }
    /**
     * Get dead-letter queue entries.
     */
    getDLQ(limit = 100) {
        if (!fs.existsSync(this.dlqFile))
            return [];
        try {
            const lines = fs.readFileSync(this.dlqFile, 'utf-8')
                .trim()
                .split('\n')
                .filter(Boolean);
            return lines
                .slice(-limit)
                .map(line => {
                try {
                    return JSON.parse(line);
                }
                catch {
                    return null;
                }
            })
                .filter((e) => e !== null);
        }
        catch {
            return [];
        }
    }
    /**
     * Retry all dead-letter queue entries.
     */
    async retryDLQ() {
        const entries = this.getDLQ();
        if (entries.length === 0)
            return [];
        const results = [];
        for (const entry of entries) {
            const endpoint = this.config.endpoints.find(e => e.name === entry.endpoint);
            if (!endpoint)
                continue;
            const result = await this.deliverToEndpoint(entry.event, endpoint);
            results.push(result);
        }
        // If all succeeded, clear the DLQ
        const allSuccess = results.every(r => r.success);
        if (allSuccess && results.length > 0) {
            try {
                fs.writeFileSync(this.dlqFile, '');
            }
            catch { /* ignore */ }
        }
        return results;
    }
    /**
     * Get delivery statistics.
     */
    getStats() {
        let dlqSize = 0;
        if (fs.existsSync(this.dlqFile)) {
            try {
                const content = fs.readFileSync(this.dlqFile, 'utf-8').trim();
                dlqSize = content ? content.split('\n').length : 0;
            }
            catch { /* ignore */ }
        }
        return {
            delivered: this.statsDelivered,
            failed: this.statsFailed,
            dlqSize,
            endpoints: this.config.endpoints.filter(e => e.enabled !== false).length,
        };
    }
    /**
     * Add an endpoint at runtime.
     */
    addEndpoint(endpoint) {
        this.config.endpoints.push(endpoint);
    }
    /**
     * Remove an endpoint by name.
     */
    removeEndpoint(name) {
        const idx = this.config.endpoints.findIndex(e => e.name === name);
        if (idx === -1)
            return false;
        this.config.endpoints.splice(idx, 1);
        return true;
    }
    // --- Private ---
    async deliverToEndpoint(event, endpoint) {
        const body = this.formatBody(event, endpoint);
        let lastError = '';
        for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {
            try {
                const statusCode = await this.sendRequest(endpoint, body);
                if (statusCode >= 200 && statusCode < 300) {
                    this.statsDelivered++;
                    return {
                        endpoint: endpoint.name,
                        success: true,
                        statusCode,
                        attempts: attempt,
                        deliveredAt: new Date().toISOString(),
                    };
                }
                lastError = `HTTP ${statusCode}`;
            }
            catch (err) {
                lastError = err.message;
            }
            // Exponential backoff before retry
            if (attempt < this.config.maxRetries) {
                const delay = this.config.retryDelayMs * Math.pow(2, attempt - 1);
                await new Promise(r => setTimeout(r, delay));
            }
        }
        // All retries exhausted -- write to DLQ
        this.statsFailed++;
        this.writeDLQ(event, endpoint.name, lastError);
        return {
            endpoint: endpoint.name,
            success: false,
            attempts: this.config.maxRetries,
            error: lastError,
        };
    }
    formatBody(event, endpoint) {
        if (endpoint.format === 'splunk-hec') {
            return JSON.stringify({
                event: {
                    ...event.data,
                    eventType: event.type,
                    source: event.source,
                    target: event.target,
                },
                time: Math.floor(new Date(event.timestamp).getTime() / 1000),
                sourcetype: 'ai:governance',
                source: event.source,
            });
        }
        // T-066: RFC 5424 syslog format
        if (endpoint.format === 'syslog') {
            return this.formatSyslog(event);
        }
        // F-NEW-CRIT-6 (2026-05-02): Elasticsearch _bulk NDJSON format.
        // Each event becomes a two-line NDJSON pair: {"index":{"_index": ...}}
        // followed by the event document. Elasticsearch's _bulk endpoint
        // requires a trailing newline on the last line. The index defaults
        // to 'connexum-governance' when not specified per-endpoint.
        if (endpoint.format === 'elastic') {
            const indexName = endpoint.elasticIndex ?? 'connexum-governance';
            const action = JSON.stringify({ index: { _index: indexName } });
            const doc = JSON.stringify({
                '@timestamp': event.timestamp,
                event_type: event.type,
                source: event.source,
                target: event.target,
                org_id: event.orgId,
                ...event.data,
            });
            return action + '\n' + doc + '\n';
        }
        // Default JSON format
        return JSON.stringify({
            id: crypto.randomUUID(),
            ...event,
            deliveredAt: new Date().toISOString(),
        });
    }
    /**
     * T-066: Format event as RFC 5424 syslog message.
     *
     * Format: <PRI>VERSION TIMESTAMP HOSTNAME APP-NAME PROCID MSGID [SD-ID SD-PARAMS] MSG
     *
     * Facility: 16 (local0), Severity mapped from event type.
     */
    formatSyslog(event) {
        // Facility 16 (local0) = 128, severity based on event type
        const severity = this.syslogSeverity(event.type);
        const pri = 128 + severity; // local0 facility (16 * 8) + severity
        const timestamp = event.timestamp; // Already ISO 8601
        const hostname = '-'; // RFC 5424 NILVALUE for unknown hostname
        const appName = 'ai-governance';
        const procId = process.pid.toString();
        const msgId = event.type.replace(/:/g, '_').toUpperCase();
        // Structured Data (SD-ID = governance@connexum)
        const sdParams = [
            `source="${this.escapeSyslog(event.source)}"`,
            event.target ? `target="${this.escapeSyslog(event.target)}"` : '',
            `eventType="${this.escapeSyslog(event.type)}"`,
        ].filter(Boolean).join(' ');
        const sd = `[governance@connexum ${sdParams}]`;
        const msg = JSON.stringify(event.data);
        return `<${pri}>1 ${timestamp} ${hostname} ${appName} ${procId} ${msgId} ${sd} ${msg}`;
    }
    syslogSeverity(eventType) {
        // RFC 5424 severity: 0=Emergency, 1=Alert, 2=Critical, 3=Error, 4=Warning, 5=Notice, 6=Info, 7=Debug
        const severityMap = {
            'governance:tampered': 1, // Alert
            'memory:tampered': 1, // Alert
            'circuit:opened': 2, // Critical
            'anomaly:detected': 3, // Error
            'approval:denied': 4, // Warning
            'approval:requested': 5, // Notice
            'approval:granted': 6, // Info
            'handoff:created': 6, // Info
            'handoff:received': 6, // Info
            'session:started': 6, // Info
            'session:ended': 6, // Info
            'circuit:closed': 6, // Info
        };
        return severityMap[eventType] ?? 6; // Default: Informational
    }
    escapeSyslog(value) {
        // RFC 5424: escape ", \, and ] in SD-PARAM values
        return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\]/g, '\\]');
    }
    sendRequest(endpoint, body) {
        return new Promise((resolve, reject) => {
            const url = new URL(endpoint.url);
            const isHttps = url.protocol === 'https:';
            const transport = isHttps ? https : http;
            // F-NEW-CRIT-6: Elasticsearch _bulk endpoint requires NDJSON content-type.
            const defaultContentType = endpoint.format === 'elastic'
                ? 'application/x-ndjson'
                : 'application/json';
            const headers = {
                'Content-Type': defaultContentType,
                'Content-Length': Buffer.byteLength(body).toString(),
                'User-Agent': '@connexum/ai-governance webhook',
                ...endpoint.headers,
            };
            // HMAC signing
            if (endpoint.secret) {
                const signature = crypto
                    .createHmac('sha256', endpoint.secret)
                    .update(body)
                    .digest('hex');
                headers['X-Governance-Signature'] = `sha256=${signature}`;
            }
            const req = transport.request({
                hostname: url.hostname,
                port: url.port || (isHttps ? 443 : 80),
                path: url.pathname + url.search,
                method: 'POST',
                headers,
                timeout: this.config.timeoutMs,
            }, (res) => {
                // Consume response body to free socket
                res.resume();
                resolve(res.statusCode || 0);
            });
            req.on('error', (err) => reject(err));
            req.on('timeout', () => {
                req.destroy(new Error('Request timeout'));
            });
            req.write(body);
            req.end();
        });
    }
    writeDLQ(event, endpointName, error) {
        const entry = {
            event,
            endpoint: endpointName,
            error,
            attempts: this.config.maxRetries,
            failedAt: new Date().toISOString(),
        };
        try {
            fs.appendFileSync(this.dlqFile, JSON.stringify(entry) + '\n');
        }
        catch {
            log.error('Failed to write to DLQ', { endpoint: endpointName });
        }
    }
}
//# sourceMappingURL=webhook-delivery.js.map