/**
 * W3C TraceContext Propagation
 *
 * Implements W3C Trace Context specification for distributed tracing.
 * See: https://www.w3.org/TR/trace-context/
 */
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomBytes } from 'node:crypto';
// W3C Trace Context version
const TRACE_VERSION = '00';
// AsyncLocalStorage for propagating trace context through async calls
const traceStorage = new AsyncLocalStorage();
// Active spans storage for span management
const spanStorage = new AsyncLocalStorage();
/**
 * Generate a 16-byte trace ID (32 hex characters)
 */
export function generateTraceId() {
    return randomBytes(16).toString('hex');
}
/**
 * Generate an 8-byte span ID (16 hex characters)
 */
export function generateSpanId() {
    return randomBytes(8).toString('hex');
}
/**
 * Build traceparent header value
 * Format: {version}-{trace-id}-{parent-id}-{trace-flags}
 * Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
 */
export function buildTraceparent(traceId, spanId, traceFlags = '01') {
    return `${TRACE_VERSION}-${traceId}-${spanId}-${traceFlags}`;
}
/**
 * Parse traceparent header value
 * Returns null if invalid
 */
export function parseTraceparent(header) {
    if (!header || typeof header !== 'string') {
        return null;
    }
    // W3C traceparent format: version-trace_id-parent_id-trace_flags
    const parts = header.trim().split('-');
    if (parts.length !== 4) {
        return null;
    }
    const [version, traceId, parentId, traceFlags] = parts;
    // Validate version (currently only '00' is supported)
    if (!version || version !== '00') {
        return null;
    }
    // Validate trace ID (32 hex characters, not all zeros)
    if (!traceId || !/^[0-9a-f]{32}$/i.test(traceId) || traceId === '0'.repeat(32)) {
        return null;
    }
    // Validate parent ID (16 hex characters, not all zeros)
    if (!parentId || !/^[0-9a-f]{16}$/i.test(parentId) || parentId === '0'.repeat(16)) {
        return null;
    }
    // Validate trace flags (2 hex characters)
    if (!traceFlags || !/^[0-9a-f]{2}$/i.test(traceFlags)) {
        return null;
    }
    return {
        version: version.toLowerCase(),
        traceId: traceId.toLowerCase(),
        parentId: parentId.toLowerCase(),
        traceFlags: traceFlags.toLowerCase(),
    };
}
/**
 * Create a new trace context
 * Optionally continue from an existing traceparent header
 */
export function createTraceContext(existingTraceparent) {
    let traceId;
    let parentSpanId;
    let traceFlags = '01'; // Sampled by default
    if (existingTraceparent) {
        const parsed = parseTraceparent(existingTraceparent);
        if (parsed) {
            traceId = parsed.traceId;
            parentSpanId = parsed.parentId;
            traceFlags = parsed.traceFlags;
        }
        else {
            // Invalid traceparent, start new trace
            traceId = generateTraceId();
        }
    }
    else {
        traceId = generateTraceId();
    }
    const spanId = generateSpanId();
    const traceparent = buildTraceparent(traceId, spanId, traceFlags);
    const context = {
        traceId,
        spanId,
        traceFlags,
        traceparent,
    };
    if (parentSpanId) {
        context.parentSpanId = parentSpanId;
    }
    return context;
}
/**
 * Get the current trace context from async local storage
 */
export function getTraceContext() {
    return traceStorage.getStore();
}
/**
 * Run a function with a specific trace context
 */
export function runWithTraceContext(context, fn) {
    return traceStorage.run(context, fn);
}
/**
 * Run a function with a new trace context (continuing from optional parent)
 */
export function runInNewTrace(fn, existingTraceparent) {
    const context = createTraceContext(existingTraceparent);
    return runWithTraceContext(context, fn);
}
/**
 * Create a child span ID in the current trace
 * Returns a new span ID while preserving the trace ID
 */
export function createChildSpanId() {
    const currentContext = getTraceContext();
    if (!currentContext) {
        throw new Error('No trace context available - must be called within runWithTraceContext');
    }
    return generateSpanId();
}
/**
 * Get trace context for logging
 * Returns object suitable for including in log entries
 */
export function getTraceLogContext() {
    const context = getTraceContext();
    if (!context) {
        return undefined;
    }
    const logContext = {
        traceId: context.traceId,
        spanId: context.spanId,
    };
    if (context.parentSpanId) {
        logContext.parentSpanId = context.parentSpanId;
    }
    return logContext;
}
/**
 * Extract trace context from HTTP headers
 */
export function extractTraceFromHeaders(headers) {
    const traceparent = headers['traceparent'];
    const tracestate = headers['tracestate'];
    // Handle array headers (some frameworks return string[])
    const traceparentStr = Array.isArray(traceparent) ? traceparent[0] : traceparent;
    const tracestateStr = Array.isArray(tracestate) ? tracestate[0] : tracestate;
    if (!traceparentStr) {
        return null;
    }
    const parsed = parseTraceparent(traceparentStr);
    if (!parsed) {
        return null;
    }
    // Create new span ID for this service
    const spanId = generateSpanId();
    const context = {
        traceId: parsed.traceId,
        spanId,
        parentSpanId: parsed.parentId,
        traceFlags: parsed.traceFlags,
        traceparent: buildTraceparent(parsed.traceId, spanId, parsed.traceFlags),
    };
    if (tracestateStr) {
        context.tracestate = tracestateStr;
    }
    return context;
}
/**
 * Inject trace context into HTTP headers
 */
export function injectTraceToHeaders(headers, context) {
    const ctx = context ?? getTraceContext();
    if (!ctx) {
        return headers;
    }
    return {
        ...headers,
        traceparent: ctx.traceparent,
        ...(ctx.tracestate ? { tracestate: ctx.tracestate } : {}),
    };
}
/**
 * Start a new span within the current trace
 */
export function startSpan(name, attributes) {
    const context = getTraceContext();
    const spans = spanStorage.getStore() ?? [];
    const parentSpan = spans[spans.length - 1];
    const parentSpanId = parentSpan?.spanId ?? context?.spanId;
    const span = {
        name,
        spanId: generateSpanId(),
        startTime: Date.now(),
        attributes: attributes ?? {},
        events: [],
        status: 'unset',
    };
    if (parentSpanId) {
        span.parentSpanId = parentSpanId;
    }
    spans.push(span);
    return span;
}
/**
 * End a span
 */
export function endSpan(span, status, errorMessage) {
    span.endTime = Date.now();
    span.status = status ?? 'ok';
    if (errorMessage) {
        span.errorMessage = errorMessage;
    }
    // Remove from active spans
    const spans = spanStorage.getStore();
    if (spans) {
        const index = spans.indexOf(span);
        if (index !== -1) {
            spans.splice(index, 1);
        }
    }
}
/**
 * Add an event to a span
 */
export function addSpanEvent(span, name, attributes) {
    const event = {
        name,
        timestamp: Date.now(),
    };
    if (attributes) {
        event.attributes = attributes;
    }
    span.events.push(event);
}
/**
 * Set span attribute
 */
export function setSpanAttribute(span, key, value) {
    span.attributes[key] = value;
}
/**
 * Run a function within a span
 */
export async function withSpan(name, fn, attributes) {
    const span = startSpan(name, attributes);
    try {
        const result = await fn(span);
        endSpan(span, 'ok');
        return result;
    }
    catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown error';
        endSpan(span, 'error', message);
        throw error;
    }
}
/**
 * Serialize trace context for queue job data
 */
export function serializeTraceContext(context) {
    const ctx = context ?? getTraceContext();
    if (!ctx) {
        return undefined;
    }
    return JSON.stringify(ctx);
}
/**
 * Deserialize trace context from queue job data
 */
export function deserializeTraceContext(data) {
    if (!data) {
        return null;
    }
    try {
        const parsed = JSON.parse(data);
        // Validate required fields
        if (parsed.traceId && parsed.spanId && parsed.traceparent) {
            return parsed;
        }
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Propagate trace context for BullMQ jobs
 * Add trace context to job data
 */
export function addTraceToJobData(data, context) {
    const serialized = serializeTraceContext(context);
    if (!serialized) {
        return data;
    }
    return {
        ...data,
        _trace: serialized,
    };
}
/**
 * Extract trace context from BullMQ job data
 */
export function extractTraceFromJobData(data) {
    const trace = data['_trace'];
    if (typeof trace !== 'string') {
        return null;
    }
    return deserializeTraceContext(trace);
}
//# sourceMappingURL=trace.js.map