/**
 * TelemetryBus — fanout to multiple {@link TelemetrySink}s, non-blocking.
 *
 * Invariants :
 *   1. Sink failures are ISOLATED — one sink throwing never affects the others
 *      nor the host agent loop. Failures are logged via the bus logger.
 *   2. The bus itself implements {@link TelemetrySink}, so it can be passed
 *      transparently to consumers expecting a single sink (composition).
 *   3. When `nonBlocking: true` (default), `start`/`step`/`finish` return
 *      immediately and dispatch in background. Errors surface via the logger.
 *   4. Backpressure : if internal queue > `maxQueueDepth`, oldest events are
 *      dropped and counted.
 *
 * Ref: command-center:sprint-693:telemetry-bus
 */
import { NOOP_TELEMETRY_SINK, } from "./port.js";
const NOOP_LOGGER = {
    warn: () => {
        /* no-op */
    },
    error: () => {
        /* no-op */
    },
};
class TelemetryBusImpl {
    name = "telemetry-bus";
    _sinks;
    _logger;
    _nonBlocking;
    _maxQueueDepth;
    /** Per-sink queue of pending events (FIFO). */
    _queues;
    /** Per-sink drain promise (single-flight). */
    _draining;
    _dropped = 0;
    _sinkErrors = 0;
    _dispatched = 0;
    constructor(opts) {
        this._sinks = opts.sinks;
        this._logger = opts.logger ?? NOOP_LOGGER;
        this._nonBlocking = opts.nonBlocking ?? true;
        this._maxQueueDepth = opts.maxQueueDepth ?? 1000;
        this._queues = this._sinks.map(() => []);
        this._draining = this._sinks.map(() => null);
    }
    get counters() {
        return {
            dropped: this._dropped,
            sinkErrors: this._sinkErrors,
            dispatched: this._dispatched,
        };
    }
    async start(event) {
        return this._fanout({
            kind: "start",
            exec: (s) => s.start(event),
        });
    }
    async step(runId, delta) {
        return this._fanout({
            kind: "step",
            exec: (s) => s.step(runId, delta),
        });
    }
    async finish(runId, event) {
        return this._fanout({
            kind: "finish",
            exec: (s) => s.finish(runId, event),
        });
    }
    async flush() {
        await Promise.all([
            ...this._draining.map((p) => p ?? Promise.resolve()),
            ...this._sinks.map((s) => s.flush?.().catch((err) => {
                this._sinkErrors += 1;
                this._logger.warn({ sink: s.name, err: errorMessage(err) }, "telemetry-bus: flush failed");
            })),
        ]);
    }
    // ── internal ───────────────────────────────────────────────────────────────
    async _fanout(evt) {
        if (this._nonBlocking) {
            // Enqueue on each sink, kick drain in background.
            this._sinks.forEach((_, i) => this._enqueue(i, evt));
            return;
        }
        // Blocking mode (tests) — dispatch sequentially and surface errors via log.
        await Promise.all(this._sinks.map((sink) => this._dispatchOne(sink, evt)));
    }
    _enqueue(sinkIdx, evt) {
        const queue = this._queues[sinkIdx];
        if (queue.length >= this._maxQueueDepth) {
            queue.shift(); // drop oldest
            this._dropped += 1;
            const sink = this._sinks[sinkIdx];
            this._logger.warn({ sink: sink.name, depth: queue.length, dropped: this._dropped }, "telemetry-bus: queue overflow, oldest dropped");
        }
        queue.push(evt);
        void this._drain(sinkIdx);
    }
    async _drain(sinkIdx) {
        if (this._draining[sinkIdx])
            return;
        const sink = this._sinks[sinkIdx];
        const queue = this._queues[sinkIdx];
        const promise = (async () => {
            while (queue.length > 0) {
                const evt = queue.shift();
                await this._dispatchOne(sink, evt);
            }
        })();
        this._draining[sinkIdx] = promise;
        try {
            await promise;
        }
        finally {
            this._draining[sinkIdx] = null;
        }
    }
    async _dispatchOne(sink, evt) {
        try {
            await evt.exec(sink);
            this._dispatched += 1;
        }
        catch (err) {
            this._sinkErrors += 1;
            this._logger.warn({
                sink: sink.name,
                kind: evt.kind,
                err: errorMessage(err),
            }, "telemetry-bus: sink dispatch failed (isolated)");
        }
    }
}
// ─── Factory ─────────────────────────────────────────────────────────────────
/**
 * Build a TelemetryBus that fans events to all sinks non-blockingly.
 *
 * If `sinks` is empty, returns {@link NOOP_TELEMETRY_SINK} so callers never
 * need to null-check.
 */
export function createTelemetryBus(opts) {
    if (opts.sinks.length === 0) {
        // Wrap NOOP with stub counters + flush for type consistency.
        return {
            ...NOOP_TELEMETRY_SINK,
            counters: { dropped: 0, sinkErrors: 0, dispatched: 0 },
            async flush() {
                /* no-op */
            },
        };
    }
    return new TelemetryBusImpl(opts);
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function errorMessage(err) {
    return err instanceof Error ? err.message : String(err);
}
//# sourceMappingURL=bus.js.map