/**
 * DestinationHealthMonitor -- per-adapter health tracking for all audit destinations.
 *
 * Tracks delivery lifecycle (attempt, success, retry, failure, DLQ write) for
 * every registered audit destination adapter: Splunk HEC, Syslog RFC 5424, and
 * the forthcoming custom-webhook, Datadog, Sentinel, and Sumo Logic destinations.
 *
 * Storage abstraction: v1 ships InMemoryHealthMetricsStore only. A future PR
 * will wire RedisHealthMetricsStore / PrismaHealthMetricsStore for production
 * persistence. See docs/observability/destination-health-monitor.md.
 *
 * Pack-binding primacy: every recordAttempt() call MUST carry a non-empty
 * packBinding[]. Empty binding throws UnboundDataError (Thomas Directive, 2026-04-22).
 *
 * Secret sanitization: this module applies a defense-in-depth sanitization pass
 * on all error strings via sanitizeErrorString(). Adapters are still responsible
 * for primary sanitization before calling recordFailure(). See PT-HEALTH-2.
 *
 * @connexum/ai-governance
 */
import { UnboundDataError } from '../errors/index.js';
import { InMemoryHealthMetricsStore, recordLatencySample, } from './health-metrics-store.js';
// ---------------------------------------------------------------------------
// Secret sanitization guard (defense in depth -- PT-HEALTH-2)
// ---------------------------------------------------------------------------
/** Patterns that indicate an embedded secret or sensitive endpoint. */
const SECRET_PATTERNS = [
    // Hex strings >= 32 chars (API tokens, SHA digests used as keys)
    { label: 'hex-secret', pattern: /\b[0-9a-fA-F]{32,}\b/g },
    // Base64 strings >= 40 chars (JWTs, encoded keys)
    { label: 'base64-secret', pattern: /[A-Za-z0-9+/]{40,}={0,2}/g },
    // Common API key prefixes
    { label: 'sk-prefix', pattern: /sk-[A-Za-z0-9_\-]{8,}/g },
    { label: 'xoxb-prefix', pattern: /xoxb-[A-Za-z0-9_\-]+/g },
    { label: 'bearer-token', pattern: /Bearer\s+[A-Za-z0-9+/._\-]+/gi },
    { label: 'shared-key', pattern: /SharedKey\s+[A-Za-z0-9+/=:_\-]+/gi },
    // Sumo Logic collector endpoints (contain embedded tokens in the hostname)
    { label: 'sumo-endpoint', pattern: /[a-zA-Z0-9\-]+\.collection\.[a-zA-Z0-9.\-]+\.sumologic\.com/g },
    // Datadog API key in URL path (/api/v2/logs?api_key=...)
    { label: 'dd-api-key', pattern: /api[-_]key=[A-Za-z0-9_\-]+/gi },
    // Splunk HEC token patterns (Splunk <token>)
    { label: 'splunk-auth', pattern: /Splunk\s+[A-Za-z0-9\-_]{8,}/g },
    // Sentinel Workspace key pattern (base64-looking value after colon)
    { label: 'sentinel-key', pattern: /[A-Za-z0-9]{8,}==[A-Za-z0-9+/=]{8,}/g },
];
/**
 * Redact common secret patterns from an error string.
 *
 * This is a defense-in-depth measure. Adapters bear primary responsibility
 * for sanitizing error strings before passing them to the monitor. This
 * function provides a second layer in case a pattern slips through.
 *
 * Matched substrings are replaced with `[REDACTED]`.
 *
 * @param input - Raw error string, possibly containing sensitive data
 * @returns Sanitized string safe for storage and logging
 */
export function sanitizeErrorString(input) {
    if (!input)
        return input;
    let result = input;
    for (const { pattern } of SECRET_PATTERNS) {
        // Reset lastIndex between calls (global flag makes RegExp stateful)
        pattern.lastIndex = 0;
        result = result.replace(pattern, '[REDACTED]');
    }
    return result;
}
/**
 * Central health monitor for all audit destination adapters.
 *
 * Usage:
 *   const monitor = new DestinationHealthMonitor();
 *   monitor.register('splunk-prod');
 *
 *   // Inside the adapter, pre-network:
 *   monitor.recordAttempt('splunk-prod', event.packBinding);
 *
 *   // Post-200:
 *   monitor.recordSuccess('splunk-prod', latencyMs);
 *
 *   // On transient retry:
 *   monitor.recordRetry('splunk-prod', '5xx', latencyMs);
 *
 *   // On terminal failure:
 *   monitor.recordFailure('splunk-prod', 'exhausted', sanitizedErr);
 *
 *   // After DLQ append:
 *   monitor.recordDlqWrite('splunk-prod');
 *
 *   // Query:
 *   const summary = monitor.getHealth('splunk-prod');
 *   const all = monitor.getHealthSummary();
 *
 * Wire-in to existing destinations is a follow-up PR (see PR body). This PR
 * ships the monitor itself; destination modifications stay out of scope.
 */
export class DestinationHealthMonitor {
    store;
    constructor(config = {}) {
        this.store = config.store ?? new InMemoryHealthMetricsStore();
    }
    // -------------------------------------------------------------------------
    // Registration
    // -------------------------------------------------------------------------
    /**
     * Register an adapter with the monitor. Must be called once per adapter
     * before any lifecycle hooks are invoked. Safe to call multiple times
     * (subsequent calls for the same id are no-ops).
     */
    register(adapterId) {
        this.store.register(adapterId);
    }
    // -------------------------------------------------------------------------
    // Lifecycle hooks
    // -------------------------------------------------------------------------
    /**
     * Record a delivery attempt (call pre-network).
     *
     * @throws UnboundDataError when packBinding is empty (PT-HEALTH-1).
     */
    recordAttempt(adapterId, packBinding) {
        if (!packBinding || packBinding.length === 0) {
            throw new UnboundDataError(`DestinationHealthMonitor.recordAttempt(${adapterId})`, adapterId);
        }
        this.ensureRegistered(adapterId);
        const rec = this.store.getMutable(adapterId);
        rec.totalAttempted += 1;
        // Per-pack accounting
        for (const packId of packBinding) {
            rec.packAttempts.set(packId, (rec.packAttempts.get(packId) ?? 0) + 1);
        }
    }
    /**
     * Record a successful delivery (call post-200).
     *
     * @param latencyMs - Round-trip time in milliseconds for this delivery.
     */
    recordSuccess(adapterId, latencyMs) {
        this.ensureRegistered(adapterId);
        const rec = this.store.getMutable(adapterId);
        rec.totalSucceeded += 1;
        rec.lastSuccessAt = Date.now();
        recordLatencySample(rec.latency, latencyMs);
    }
    /**
     * Record a transient retry (call per retry before the next attempt).
     *
     * @param reason - HTTP category or network error type.
     * @param latencyMs - Time elapsed for the failed attempt in milliseconds.
     */
    recordRetry(adapterId, reason, latencyMs) {
        this.ensureRegistered(adapterId);
        const rec = this.store.getMutable(adapterId);
        rec.totalRetried += 1;
        // Capture latency for the failed attempt (contributes to distribution)
        recordLatencySample(rec.latency, latencyMs);
        // reason is stored implicitly via totalRetried; callers may extend the
        // store interface to break it down further in a future PR.
        void reason;
    }
    /**
     * Record a terminal failure (call when the event is DLQ-bound or dropped).
     *
     * The sanitizedError string is passed through sanitizeErrorString() as a
     * second-pass defense before storage. Adapters must still sanitize first.
     *
     * @param terminalReason - Categorized failure type.
     * @param sanitizedError - Pre-sanitized error description. MUST NOT contain
     *   secrets, endpoint URLs with embedded keys, or HMAC signatures.
     */
    recordFailure(adapterId, terminalReason, sanitizedError) {
        this.ensureRegistered(adapterId);
        const rec = this.store.getMutable(adapterId);
        rec.totalFailed += 1;
        rec.lastErrorAt = Date.now();
        // Defense-in-depth: sanitize again even if adapter already sanitized
        rec.lastErrorMessage = sanitizeErrorString(sanitizedError);
        // terminalReason contributes to totalFailed; a future PR may add per-reason
        // breakdown to the store interface.
        void terminalReason;
    }
    /**
     * Record a DLQ write (call after appending to the dead-letter queue file).
     * Increments dlqDepth; depth is NOT automatically decremented on drain --
     * call resetDlqDepth() after a successful drain cycle.
     */
    recordDlqWrite(adapterId) {
        this.ensureRegistered(adapterId);
        const rec = this.store.getMutable(adapterId);
        rec.dlqDepth += 1;
    }
    /**
     * Reset DLQ depth counter after a successful drain cycle.
     * Does not reset other counters.
     */
    resetDlqDepth(adapterId) {
        this.ensureRegistered(adapterId);
        const rec = this.store.getMutable(adapterId);
        rec.dlqDepth = 0;
    }
    /**
     * Update the circuit-breaker state for an adapter.
     * Call this whenever the adapter's circuit-breaker state transitions.
     * Pass null if the adapter does not have a circuit breaker.
     */
    setCircuitBreakerState(adapterId, state) {
        this.ensureRegistered(adapterId);
        const rec = this.store.getMutable(adapterId);
        rec.circuitBreakerState = state;
    }
    // -------------------------------------------------------------------------
    // Query API
    // -------------------------------------------------------------------------
    /**
     * Return a health summary for a single adapter, or null if not registered.
     */
    getHealth(adapterId) {
        return this.store.getSummary(adapterId);
    }
    /**
     * Return health summaries for all registered adapters.
     */
    getHealthSummary() {
        return this.store.getAllSummaries();
    }
    // -------------------------------------------------------------------------
    // Internals
    // -------------------------------------------------------------------------
    /**
     * Auto-register an adapter if not yet registered.
     * This is a convenience for adapters that call lifecycle hooks before
     * explicitly calling register(). Explicit register() is still preferred.
     */
    ensureRegistered(adapterId) {
        this.store.register(adapterId);
    }
}
//# sourceMappingURL=destination-health-monitor.js.map