/**
 * FanoutArtifactWriter — layers multiple writers so each `emit()` fans out
 * to every configured backend.
 *
 * Default wiring (W0064 reorder of D0033 M4):
 *   `FanoutArtifactWriter([ GcsArtifactWriter, LocalFilesystemArtifactWriter ])`
 *
 * Semantics:
 * - Fan out in declaration order. Every writer runs, even if earlier ones fail.
 * - Return the **first non-null ArtifactRef**. The remote backend is listed
 *   first so its ref wins when both succeed — the published manifest points
 *   at a cross-machine-readable store. A remote failure falls through to
 *   local, so a local success + remote failure still produces a non-null ref
 *   pointing at local and the pipeline succeeds with a warning on the remote
 *   leg.
 * - Failures on individual writers warn (via their own P5 paths) but do
 *   not propagate. The fanout never throws.
 *
 * This writer is a composition primitive — it adds no I/O of its own.
 * Tests verify the fanout semantics against `NoOpArtifactWriter` instances
 * plus a recording test double.
 *
 * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M4)
 * @see docs/design-docs/canonical-artifact-ref-selection.md — W0064 ordering
 */
export class FanoutArtifactWriter {
    writers;
    progress;
    constructor(writers, options = {}) {
        if (writers.length === 0) {
            throw new Error("FanoutArtifactWriter requires at least one delegate writer");
        }
        this.writers = writers;
        this.progress = options.progress;
    }
    reportProgress(ref) {
        if (!this.progress)
            return;
        this.progress.reporter.phaseProgress({
            phaseId: this.progress.phaseId,
            items: 1,
            bytes: ref.bytes,
            label: ref.path,
        });
    }
    async emit(type, association, payload) {
        const refs = await this.runAll((w) => w.emit(type, association, payload));
        const ref = firstNonNull(refs);
        if (ref)
            this.reportProgress(ref);
        return ref;
    }
    async appendNdjson(type, association, rows) {
        const refs = await this.runAll((w) => w.appendNdjson(type, association, rows));
        const ref = firstNonNull(refs);
        if (ref)
            this.reportProgress(ref);
        return ref;
    }
    async writeManifest(runId, manifest) {
        const refs = await this.runAll((w) => w.writeManifest(runId, manifest));
        const ref = firstNonNull(refs);
        if (ref)
            this.reportProgress(ref);
        return ref;
    }
    /** @deprecated Use `emit()` instead. */
    async writeBulk(type, runId, data) {
        const refs = await this.runAll((w) => w.writeBulk(type, runId, data));
        return firstNonNull(refs);
    }
    /** @deprecated Use `emit()` per entry instead. */
    async writePerEntry(type, runId, entries) {
        const refs = await this.runAll((w) => w.writePerEntry(type, runId, entries));
        return firstNonNull(refs);
    }
    /**
     * Run `op` against every delegate writer. Any individual writer's rejected
     * promise is caught and logged; the fanout itself never rejects.
     */
    async runAll(op) {
        return Promise.all(this.writers.map(async (writer) => {
            try {
                return await op(writer);
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                console.warn(`  ⚠️  Fanout delegate threw (non-blocking): ${writer.constructor.name} — ${message}`);
                return null;
            }
        }));
    }
}
function firstNonNull(refs) {
    for (const ref of refs) {
        if (ref !== null)
            return ref;
    }
    return null;
}
