import { SpanKind, SpanStatusCode } from './types.js';
// ── ID generation ───────────────────────────────────────────────────────────────
/** Generate a 128-bit trace ID (32-char lowercase hex). */
function generateTraceId() {
    return [
        crypto.getRandomValues(new Uint8Array(16)),
    ].map((bytes) => Array.from(bytes)
        .map((b) => b.toString(16).padStart(2, '0'))
        .join(''))[0];
}
/** Generate a 64-bit span ID (16-char lowercase hex). */
function generateSpanId() {
    const bytes = crypto.getRandomValues(new Uint8Array(8));
    return Array.from(bytes)
        .map((b) => b.toString(16).padStart(2, '0'))
        .join('');
}
/** Check if a hex string is non-zero. */
function isNonZeroHex(hex) {
    return /[1-9a-f]/.test(hex);
}
// ── SpanImpl ──────────────────────────────────────────────────────────────────────
class SpanImpl {
    spanContext;
    parentSpanId;
    _ended = false;
    _attributes = {};
    _events = [];
    _status = { code: SpanStatusCode.UNSET };
    _startTimeMs;
    _endTimeMs = 0;
    _name;
    _kind;
    _scope;
    _onEnd;
    constructor(opts) {
        this._name = opts.name;
        this._kind = opts.kind;
        this._scope = opts.scope;
        this._startTimeMs = opts.startTimeMs ?? Date.now();
        this._onEnd = opts.onEnd;
        this.parentSpanId = opts.parentSpanId;
        const spanId = generateSpanId();
        this.spanContext = {
            traceId: opts.traceId,
            spanId,
            isValid: isNonZeroHex(opts.traceId) && isNonZeroHex(spanId),
        };
    }
    get ended() {
        return this._ended;
    }
    setAttribute(key, value) {
        if (this._ended)
            return this;
        this._attributes[key] = value;
        return this;
    }
    setAttributes(attrs) {
        if (this._ended)
            return this;
        Object.assign(this._attributes, attrs);
        return this;
    }
    addEvent(name, attributes) {
        if (this._ended)
            return this;
        this._events.push({ name, timestamp: Date.now(), attributes });
        return this;
    }
    setStatus(status) {
        if (this._ended)
            return this;
        // Only downgrade if current is UNSET; ERROR takes priority over OK
        if (this._status.code === SpanStatusCode.UNSET || status.code === SpanStatusCode.ERROR) {
            this._status = status;
        }
        return this;
    }
    recordException(error) {
        if (this._ended)
            return this;
        this.addEvent('exception', {
            'exception.type': error.name,
            'exception.message': error.message,
            'exception.stacktrace': error.stack ?? '',
        });
        this.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
        return this;
    }
    end(endTimeMs) {
        if (this._ended)
            return;
        this._ended = true;
        this._endTimeMs = endTimeMs ?? Date.now();
        this._onEnd(this.toReadable());
    }
    toReadable() {
        const endMs = this._endTimeMs > 0 ? this._endTimeMs : Date.now();
        return Object.freeze({
            name: this._name,
            kind: this._kind,
            spanContext: this.spanContext,
            parentSpanId: this.parentSpanId,
            startTimeMs: this._startTimeMs,
            endTimeMs: endMs,
            durationMs: endMs - this._startTimeMs,
            attributes: { ...this._attributes },
            events: [...this._events],
            status: this._status,
            instrumentationScope: this._scope,
        });
    }
}
// ── RuntimeTracer ──────────────────────────────────────────────────────────────
/**
 * RuntimeTracer — creates spans and routes completed spans to exporters.
 *
 * Usage:
 * ```ts
 * const span = tracer.startSpan('turn.lifecycle', { traceId, parentSpanId });
 * span.setAttribute('turn.id', turnId);
 * span.end();
 * ```
 */
export class RuntimeTracer {
    config;
    constructor(config) {
        this.config = config;
    }
    /**
     * Start a new span.
     *
     * @param name - Human-readable span name.
     * @param opts - Optional trace/parent context and kind.
     */
    startSpan(name, opts) {
        if (!this.config.enabled) {
            // Return a no-op span when tracing is disabled
            return new NoopSpan();
        }
        const traceId = opts?.traceId ?? generateTraceId();
        const span = new SpanImpl({
            name,
            kind: opts?.kind ?? SpanKind.INTERNAL,
            traceId,
            parentSpanId: opts?.parentSpanId,
            startTimeMs: opts?.startTimeMs,
            scope: this.config.scope,
            onEnd: (readable) => this._export([readable]),
        });
        if (opts?.attributes) {
            span.setAttributes(opts.attributes);
        }
        return span;
    }
    /**
     * Flush all exporters. Call during graceful shutdown.
     */
    async flush() {
        await Promise.allSettled(this.config.exporters.map((e) => e.flush()));
    }
    /**
     * Shut down all exporters.
     */
    async shutdown() {
        await Promise.allSettled(this.config.exporters.map((e) => e.shutdown()));
    }
    /** Fire-and-forget export to all registered exporters. */
    _export(spans) {
        for (const exporter of this.config.exporters) {
            // Export is fire-and-forget; failures must not propagate
            exporter.export(spans).catch(() => {
                // Export failure — non-fatal, swallowed intentionally
            });
        }
    }
}
// ── NoopSpan ───────────────────────────────────────────────────────────────────────
/** No-op span returned when tracing is disabled. All operations are safe no-ops. */
class NoopSpan {
    spanContext = {
        traceId: '00000000000000000000000000000000',
        spanId: '0000000000000000',
        isValid: false,
    };
    parentSpanId;
    ended = false;
    setAttribute(_key, _value) { return this; }
    setAttributes(_attrs) { return this; }
    addEvent(_name, _attributes) { return this; }
    setStatus(_status) { return this; }
    recordException(_error) { return this; }
    end(_endTimeMs) { }
    toReadable() {
        return {
            name: 'noop',
            kind: SpanKind.INTERNAL,
            spanContext: this.spanContext,
            parentSpanId: undefined,
            startTimeMs: 0,
            endTimeMs: 0,
            durationMs: 0,
            attributes: {},
            events: [],
            status: { code: SpanStatusCode.UNSET },
            instrumentationScope: 'noop',
        };
    }
}
