/**
 * Optional OpenTelemetry integration.
 *
 * Design goals:
 * - No hard dependency on SDKs/exporters (apps bring their own).
 * - Safe to import in runtimes without tracing configured (no-op behavior).
 * - Best-effort: never breaks request handling.
 *
 * Tiny Node enablement snippet (exporter is app-owned):
 *
 *   npm i @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node \
 *     @opentelemetry/exporter-trace-otlp-http
 *
 *   // in your app entrypoint (before creating the server)
 *   import { NodeSDK } from '@opentelemetry/sdk-node';
 *   import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
 *   import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
 *
 *   const sdk = new NodeSDK({
 *     traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }),
 *     instrumentations: [getNodeAutoInstrumentations()],
 *   });
 *   await sdk.start();
 *   process.env.OTEL_ENABLED = 'true';
 */
import { Env } from '../config/env.js';
import { createRequire } from '../node-singletons/module.js';
let cachedApi;
const resolveOpenTelemetryApi = () => {
    if (cachedApi !== undefined)
        return cachedApi;
    // Check for injected API (used for testing)
    const injected = globalThis
        .__zintrustOpenTelemetryApi;
    if (injected !== undefined && injected !== null) {
        cachedApi = injected;
        return cachedApi;
    }
    try {
        const require = createRequire(import.meta.url);
        cachedApi = require('@opentelemetry/api');
        return cachedApi;
    }
    catch {
        cachedApi = null;
        return null;
    }
};
const noopSpan = {
    setAttribute: () => undefined,
    updateName: () => undefined,
    setStatus: () => undefined,
    end: () => undefined,
};
const fallbackContext = {
    active: () => ({}),
    with: (_ctx, fn, _thisArg, ...args) => fn(...args),
};
const fallbackPropagation = {
    extract: (_ctx) => _ctx,
    inject: () => undefined,
};
const fallbackTrace = {
    getTracer: () => ({
        startSpan: (_name, _options, _context) => noopSpan,
    }),
    getSpan: (_ctx) => undefined,
    setSpan: (ctx) => ctx,
};
const fallbackSpanKind = {
    SERVER: 1,
    CLIENT: 2,
};
const fallbackSpanStatusCode = {
    OK: 1,
    ERROR: 2,
};
const resolveSpanStatusCode = () => {
    return otel()?.SpanStatusCode ?? fallbackSpanStatusCode;
};
const resolveTracingContext = () => {
    const api = otel();
    return {
        traceApi: api?.trace ?? fallbackTrace,
        activeContext: api?.context?.active() ?? fallbackContext.active(),
        spanKind: api?.SpanKind ?? fallbackSpanKind,
    };
};
const otel = () => resolveOpenTelemetryApi();
const isEnabled = () => {
    return Env.getBool('OTEL_ENABLED', false);
};
const extractContextFromHeaders = (req) => {
    const active = otel()?.context?.active() ?? fallbackContext.active();
    try {
        const extractor = otel()?.propagation?.extract ?? fallbackPropagation.extract;
        return extractor(active, req, {
            get(carrier, key) {
                const value = carrier.getHeader(key);
                if (typeof value === 'string')
                    return value;
                if (Array.isArray(value))
                    return value.join(',');
                return undefined;
            },
            keys() {
                return [];
            },
        });
    }
    catch {
        return active;
    }
};
const startHttpServerSpan = (req, input) => {
    const parent = extractContextFromHeaders(req);
    const tracer = (otel()?.trace ?? fallbackTrace).getTracer('zintrust');
    const span = tracer.startSpan(`${input.method} unmatched`, {
        kind: (otel()?.SpanKind ?? fallbackSpanKind).SERVER,
        attributes: {
            'http.method': input.method,
            'http.target': input.path,
            'http.user_agent': input.userAgent ?? '',
            'service.name': input.serviceName ?? '',
            'enduser.id': input.userId ?? '',
            'zintrust.tenant_id': input.tenantId ?? '',
            'zintrust.request_id': input.requestId ?? '',
        },
    }, parent);
    const spanContext = (otel()?.trace ?? fallbackTrace).setSpan(parent, span);
    return { span, context: spanContext };
};
const runWithContext = async (ctx, fn) => {
    const runner = otel()?.context?.with ?? fallbackContext.with;
    return runner(ctx, fn);
};
const setHttpRoute = (span, method, route) => {
    try {
        span.setAttribute('http.route', route);
        span.updateName(`${method} ${route}`);
    }
    catch {
        // best-effort
    }
};
const endHttpServerSpan = (span, input) => {
    try {
        if (input.route !== undefined && input.route.trim() !== '') {
            span.setAttribute('http.route', input.route);
        }
        if (typeof input.status === 'number') {
            span.setAttribute('http.status_code', input.status);
            if (input.status >= 500) {
                span.setStatus({ code: (otel()?.SpanStatusCode ?? fallbackSpanStatusCode).ERROR });
            }
            else {
                span.setStatus({ code: (otel()?.SpanStatusCode ?? fallbackSpanStatusCode).OK });
            }
        }
        if (input.error !== undefined) {
            // Avoid throwing; represent as a string attribute.
            span.setAttribute('zintrust.error', input.error instanceof Error ? input.error.message : String(input.error));
            span.setStatus({ code: (otel()?.SpanStatusCode ?? fallbackSpanStatusCode).ERROR });
        }
        span.end();
    }
    catch {
        // best-effort
    }
};
const injectTraceHeaders = (headers) => {
    try {
        const injector = otel()?.propagation?.inject ?? fallbackPropagation.inject;
        injector(otel()?.context?.active() ?? fallbackContext.active(), headers);
    }
    catch {
        // best-effort
    }
    return headers;
};
const mapDbSystem = (driver) => {
    switch (driver) {
        case 'postgresql':
            return 'postgresql';
        case 'mysql':
            return 'mysql';
        case 'sqlserver':
            return 'mssql';
        case 'd1':
        case 'd1-remote':
        case 'sqlite':
        default:
            return 'sqlite';
    }
};
const recordDbQuerySpan = (input) => {
    if (isEnabled() === false)
        return;
    try {
        // Only create a DB span if we're already inside a request trace.
        const traceApi = otel()?.trace ?? fallbackTrace;
        const parentSpan = traceApi.getSpan(otel()?.context?.active() ?? fallbackContext.active());
        if (!parentSpan)
            return;
        const tracer = traceApi.getTracer('zintrust');
        const now = Date.now();
        const durationMs = Number.isFinite(input.durationMs) ? Math.max(0, input.durationMs) : 0;
        const startTime = now - durationMs;
        const span = tracer.startSpan('db.query', {
            kind: (otel()?.SpanKind ?? fallbackSpanKind).CLIENT,
            startTime,
            attributes: {
                'db.system': mapDbSystem(input.driver),
                'db.operation': 'query',
                'zintrust.db.driver': input.driver,
            },
        }, otel()?.context?.active() ?? fallbackContext.active());
        span.end(now);
    }
    catch {
        // best-effort
    }
};
const recordQueueOperationSpan = (input) => {
    if (isEnabled() === false)
        return;
    try {
        const tracing = resolveTracingContext();
        const parentSpan = tracing.traceApi.getSpan(tracing.activeContext);
        if (!parentSpan)
            return;
        const tracer = tracing.traceApi.getTracer('zintrust');
        const now = Date.now();
        const durationMs = Number.isFinite(input.durationMs) ? Math.max(0, input.durationMs) : 0;
        const startTime = now - durationMs;
        const span = tracer.startSpan(`queue.${input.operation}`, {
            kind: tracing.spanKind.CLIENT,
            startTime,
            attributes: {
                'messaging.system': 'zintrust-queue',
                'messaging.operation': input.operation,
                'messaging.destination': input.queueName,
                'zintrust.queue.status': input.status,
            },
        }, tracing.activeContext);
        const statusCode = resolveSpanStatusCode();
        if (input.status === 'error') {
            span.setStatus({ code: statusCode.ERROR });
        }
        else {
            span.setStatus({ code: statusCode.OK });
        }
        span.end(now);
    }
    catch {
        // best-effort
    }
};
export const OpenTelemetry = Object.freeze({
    isEnabled,
    startHttpServerSpan,
    runWithContext,
    setHttpRoute,
    endHttpServerSpan,
    injectTraceHeaders,
    recordDbQuerySpan,
    recordQueueOperationSpan,
});
export default OpenTelemetry;
