/**
 * ApiGatewayArtifactWriter — uploads AILF run artifacts via the API Gateway.
 *
 * Used when the CLI runs locally without GCS credentials. The Gateway signs a
 * PUT URL (scoped to a single GCS object) and the writer PUTs JSON directly
 * to GCS so Vercel stays out of the data path.
 *
 * Endpoints:
 *   - Bulk:      GET  {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/upload-url
 *   - Per-entry: GET  {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/{entryKey}/upload-url
 *   - Manifest:  GET  {apiBaseUrl}/v1/runs/{runId}/artifacts/upload-url
 *
 * ## W0049 API surface
 *
 * - `emit(type, association, payload)` — canonical single-shot write. Uses
 *   the registry to resolve `layout` and the signing endpoint.
 * - `appendNdjson` — NOT IMPLEMENTED. The API Gateway has no batch-signing
 *   endpoint yet (W0052), and per-object signing would issue one sign call
 *   per row, which blows the Vercel Function budget. Throws
 *   `NotImplementedError` so the gap is explicit rather than a silent no-op.
 * - `writeBulk` / `writePerEntry` — @deprecated legacy surface; removal in W0052.
 *
 * Design principles:
 * - P5: Non-blocking — any non-structural failure returns null and warns.
 * - Stateless — no client state between calls.
 *
 * @see docs/decisions/D0032-run-anchored-artifact-store.md
 * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
 */
import { ARTIFACT_REGISTRY, assertWritePolicyMatches, NotImplementedError, } from "../_vendor/ailf-core/index.js";
import { prepareUploadBody } from "./prepare-upload-body.js";
import { NO_OP_UPLOAD_METRICS, } from "./upload-metrics.js";
export class ApiGatewayArtifactWriter {
    options;
    metrics;
    writerSource;
    constructor(options) {
        this.options = options;
        this.metrics = options.metrics ?? NO_OP_UPLOAD_METRICS;
        this.writerSource = options.writerSource ?? "pipeline";
    }
    // ---- Canonical W0049 API ------------------------------------------------
    async emit(type, association, payload) {
        const descriptor = ARTIFACT_REGISTRY[type];
        assertWritePolicyMatches(this.writerSource, descriptor);
        const runId = association.run;
        if (!runId) {
            console.warn(`  ⚠️  emit("${type}"): association.run is required, skipping`);
            return null;
        }
        if (descriptor.layout === "bulk") {
            const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/upload-url`;
            return this.putJson(uploadUrlPath, payload, {
                layout: "bulk",
                entryCount: entryCountOf(payload),
                type,
            });
        }
        // per-entry
        const entryKey = descriptor.formatEntryKey(association);
        const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/${encodeURIComponent(entryKey)}/upload-url`;
        const result = await this.putJsonRaw(uploadUrlPath, payload, type);
        if (!result)
            return null;
        return {
            store: "gcs",
            bucket: result.bucket,
            path: `runs/${runId}/${descriptor.slug}`,
            bytes: result.bytes,
            entryCount: 1,
            layout: "per-entry",
            entries: [{ key: entryKey, bytes: result.bytes, association }],
        };
    }
    async appendNdjson() {
        throw new NotImplementedError("ApiGatewayArtifactWriter.appendNdjson is not supported. " +
            "NDJSON streaming for traces requires the batch signing endpoint " +
            "(W0052). Producers should use GcsArtifactWriter directly when " +
            "running locally, or defer traces emission until the gateway lands " +
            "the batch route.");
    }
    async writeManifest(runId, manifest) {
        const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/upload-url`;
        return this.putJson(uploadUrlPath, manifest, {
            layout: "bulk",
            type: "manifest",
        });
    }
    // ---- Deprecated legacy surface (W0052) ----------------------------------
    /** @deprecated Use `emit()` instead. */
    async writeBulk(type, runId, data) {
        const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/upload-url`;
        return this.putJson(uploadUrlPath, data, {
            layout: "bulk",
            entryCount: entryCountOf(data),
            type,
        });
    }
    /** @deprecated Use `emit()` per entry instead. */
    async writePerEntry(type, runId, entries) {
        const descriptor = ARTIFACT_REGISTRY[type];
        if (!descriptor.parseEntryKey) {
            console.warn(`  ⚠️  writePerEntry called for "${type}" but the registry has no parseEntryKey`);
            return null;
        }
        const uploaded = [];
        let totalBytes = 0;
        let bucket = this.options.bucket;
        for (const entry of entries) {
            const parsed = descriptor.parseEntryKey(entry.key);
            if (!parsed.ok) {
                console.warn(`  ⚠️  Skipping entry with invalid key "${entry.key}": ${parsed.reason}`);
                continue;
            }
            const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/${encodeURIComponent(entry.key)}/upload-url`;
            const result = await this.putJsonRaw(uploadUrlPath, entry.data, type);
            if (!result)
                continue;
            bucket = result.bucket;
            uploaded.push({ key: entry.key, bytes: result.bytes });
            totalBytes += result.bytes;
        }
        if (uploaded.length === 0)
            return null;
        return {
            store: "gcs",
            bucket,
            path: `runs/${runId}/${descriptor.slug}`,
            bytes: totalBytes,
            entryCount: uploaded.length,
            layout: "per-entry",
            entries: uploaded,
        };
    }
    // ---- Internals ----------------------------------------------------------
    async putJson(uploadUrlPath, data, meta) {
        const result = await this.putJsonRaw(uploadUrlPath, data, meta.type);
        if (!result)
            return null;
        return {
            store: "gcs",
            bucket: result.bucket,
            path: result.path,
            bytes: result.bytes,
            entryCount: meta.entryCount,
            layout: meta.layout,
        };
    }
    async putJsonRaw(uploadUrlPath, data, type) {
        // W0059 — redact + serialize through the shared helper so the gateway
        // path strips secrets before they reach the signed GCS URL.
        const { body, bytes } = prepareUploadBody(data, "application/json");
        try {
            const signed = await this.fetchSignedUrl(uploadUrlPath, type);
            if (!signed)
                return null;
            const putStart = Date.now();
            let putSuccess = false;
            try {
                const putRes = await fetch(signed.url, {
                    body,
                    headers: signed.requiredHeaders,
                    method: "PUT",
                });
                if (!putRes.ok) {
                    console.warn(`  ⚠️  Artifact upload failed (non-blocking): ${signed.path} — GCS PUT ${putRes.status} ${putRes.statusText}`);
                    return null;
                }
                putSuccess = true;
                return { bucket: signed.bucket, path: signed.path, bytes };
            }
            finally {
                this.metrics.record({
                    phase: "put",
                    writer: "ApiGatewayArtifactWriter",
                    type,
                    ms: Date.now() - putStart,
                    bytes,
                    success: putSuccess,
                });
            }
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            console.warn(`  ⚠️  Artifact upload failed (non-blocking): ${uploadUrlPath} — ${message}`);
            return null;
        }
    }
    async fetchSignedUrl(uploadUrlPath, type) {
        const start = Date.now();
        let success = false;
        try {
            const url = `${this.options.apiBaseUrl.replace(/\/$/, "")}${uploadUrlPath}`;
            const res = await fetch(url, {
                headers: { Authorization: `Bearer ${this.options.apiKey}` },
                method: "GET",
            });
            if (!res.ok) {
                console.warn(`  ⚠️  Signed-URL request failed: ${res.status} ${res.statusText}`);
                return null;
            }
            const body = (await res.json());
            if (body.object !== "signed_upload_url" ||
                typeof body.url !== "string" ||
                typeof body.path !== "string" ||
                typeof body.bucket !== "string" ||
                !body.requiredHeaders) {
                console.warn(`  ⚠️  Signed-URL response was malformed`);
                return null;
            }
            success = true;
            return {
                bucket: body.bucket,
                method: "PUT",
                object: "signed_upload_url",
                path: body.path,
                requiredHeaders: body.requiredHeaders,
                url: body.url,
            };
        }
        finally {
            this.metrics.record({
                phase: "sign",
                writer: "ApiGatewayArtifactWriter",
                type,
                ms: Date.now() - start,
                success,
            });
        }
    }
}
function entryCountOf(data) {
    if (typeof data === "object" &&
        data !== null &&
        "entries" in data &&
        typeof data.entries === "object") {
        return Object.keys(data.entries)
            .length;
    }
    return undefined;
}
