/**
 * UploadMetrics — spike instrumentation for W0056 (faster artifact upload).
 *
 * Captures per-operation timing on the artifact-upload path so the spike has
 * a measured baseline: artifact count, total bytes, wall-clock, and the
 * sign-RTT vs. PUT split. Gated on `AILF_UPLOAD_METRICS=1` in the composition
 * root — a no-op when off.
 *
 * Design:
 * - `UploadMetricsSink` is the narrow interface that writers depend on.
 * - `UploadMetrics` is the in-process implementation that buffers events
 *   and emits both a stderr summary table and an NDJSON detail file.
 * - `summarize()` is called by `InstrumentedArtifactWriter` once, after
 *   `writeManifest` succeeds (the natural end-of-run signal).
 *
 * This file is a spike deliverable — the API is intentionally ad hoc and
 * may be promoted to `packages/core/src/ports/` if we ship anything.
 */
import { mkdir, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
// ---------------------------------------------------------------------------
// Implementation
// ---------------------------------------------------------------------------
/**
 * No-op sink — writers default to this when metrics are off, so the
 * instrumentation call sites remain uniform whether or not the collector is
 * active.
 */
export const NO_OP_UPLOAD_METRICS = {
    record() { },
};
export class UploadMetrics {
    options;
    events = [];
    summarized = false;
    constructor(options) {
        this.options = options;
    }
    record(event) {
        this.events.push({ ...event, ts: new Date().toISOString() });
    }
    async summarize() {
        if (this.summarized)
            return;
        this.summarized = true;
        const { logger, detailFile } = this.options;
        if (this.events.length === 0) {
            logger.info("[upload-metrics] no events recorded");
            return;
        }
        const table = buildSummaryTable(this.events);
        logger.info(`[upload-metrics] ${this.events.length} events recorded\n${table}`);
        if (detailFile) {
            try {
                await mkdir(dirname(detailFile), { recursive: true });
                const body = this.events.map((e) => JSON.stringify(e)).join("\n") + "\n";
                await writeFile(detailFile, body, "utf-8");
                logger.info(`[upload-metrics] detail written to ${detailFile}`);
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                logger.warn(`[upload-metrics] failed to write detail file "${detailFile}": ${message}`);
            }
        }
    }
    /** Exposed for tests — returns a copy. */
    snapshot() {
        return [...this.events];
    }
}
export function buildSummaryTable(events) {
    const byKey = new Map();
    for (const ev of events) {
        const key = `${ev.phase}\t${ev.writer}`;
        let bucket = byKey.get(key);
        if (!bucket) {
            bucket = [];
            byKey.set(key, bucket);
        }
        bucket.push(ev);
    }
    const rows = [];
    for (const [key, bucket] of byKey) {
        const phase = key.split("\t").join(" · ");
        const durations = bucket.map((e) => e.ms).sort((a, b) => a - b);
        const totalMs = durations.reduce((sum, ms) => sum + ms, 0);
        const totalBytes = bucket.reduce((sum, e) => sum + (e.bytes ?? 0), 0);
        const failures = bucket.filter((e) => !e.success).length;
        rows.push({
            phase,
            count: bucket.length,
            failures,
            totalMs,
            totalBytes,
            p50: percentile(durations, 0.5),
            p95: percentile(durations, 0.95),
            max: durations[durations.length - 1] ?? 0,
        });
    }
    rows.sort((a, b) => b.totalMs - a.totalMs);
    const header = "phase                                          |  n  | fail |  bytes      | total ms | p50 | p95 | max";
    const sep = "-----------------------------------------------+-----+------+-------------+----------+-----+-----+-----";
    const body = rows
        .map((r) => `${pad(r.phase, 47)}| ${pad(String(r.count), 4)}| ${pad(String(r.failures), 5)}| ${pad(formatBytes(r.totalBytes), 12)}| ${pad(String(Math.round(r.totalMs)), 9)}| ${pad(String(Math.round(r.p50)), 4)}| ${pad(String(Math.round(r.p95)), 4)}| ${Math.round(r.max)}`)
        .join("\n");
    return `${header}\n${sep}\n${body}`;
}
function percentile(sorted, p) {
    if (sorted.length === 0)
        return 0;
    const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * p) - 1));
    return sorted[idx] ?? 0;
}
function pad(s, width) {
    return s.length >= width ? `${s} ` : s + " ".repeat(width - s.length);
}
function formatBytes(n) {
    if (n < 1024)
        return `${n} B`;
    if (n < 1024 * 1024)
        return `${(n / 1024).toFixed(1)} KB`;
    if (n < 1024 * 1024 * 1024)
        return `${(n / 1024 / 1024).toFixed(1)} MB`;
    return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
