// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
import fp from 'fastify-plugin';
import { trace, context, SpanKind, SpanStatusCode, } from '@opentelemetry/api';
import { createLogger } from '../logger.js';
import { getTracer, VorionTracers } from './tracer.js';
import { VorionSpanAttributes } from './spans.js';
import { extractContextFromHeaders, injectContextToHeaders, getTraceContext, } from './propagation.js';
const logger = createLogger({ component: 'telemetry-middleware' });
/**
 * Default paths to exclude from tracing
 */
const DEFAULT_EXCLUDE_PATHS = [
    '/health',
    '/ready',
    '/metrics',
    '/favicon.ico',
    '/.well-known',
];
/**
 * Get span name from request
 */
function getSpanName(request) {
    // Use route pattern if available
    const routePattern = request
        .routeOptions?.url;
    if (routePattern) {
        return `${request.method} ${routePattern}`;
    }
    // Fall back to URL path
    return `${request.method} ${request.url.split('?')[0]}`;
}
/**
 * Get HTTP attributes for span
 */
function getHttpAttributes(request, reply) {
    const attributes = {
        'http.method': request.method,
        'http.url': request.url,
        'http.target': request.url,
        'http.host': request.hostname,
        'http.scheme': request.protocol,
        'http.user_agent': request.headers['user-agent'] ?? 'unknown',
        'net.peer.ip': request.ip,
    };
    // Add route pattern if available
    const routePattern = request
        .routeOptions?.url;
    if (routePattern) {
        attributes['http.route'] = routePattern;
    }
    // Add response attributes
    if (reply) {
        attributes['http.status_code'] = reply.statusCode;
    }
    // Add request ID
    const requestId = request.headers['x-request-id'];
    if (requestId) {
        attributes['http.request_id'] = Array.isArray(requestId)
            ? requestId[0]
            : requestId;
    }
    return attributes;
}
/**
 * Default security context extractor
 */
function defaultTracingSecurityContextExtractor(request) {
    // Try to extract from JWT user
    const user = request.user;
    if (user) {
        return {
            tenantId: user.tenantId,
            entityId: user.entityId,
            userId: user.sub,
            roles: user.roles,
            sessionId: user.sid,
        };
    }
    // Try to extract from headers
    const tenantId = request.headers['x-tenant-id'];
    if (tenantId) {
        return {
            tenantId: Array.isArray(tenantId) ? tenantId[0] : tenantId,
        };
    }
    return null;
}
/**
 * Tracing middleware plugin
 */
const tracingMiddlewarePlugin = (fastify, options, done) => {
    // Skip if disabled
    if (options.enabled === false) {
        logger.info('Tracing middleware disabled');
        done();
        return;
    }
    const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
    const addResponseHeaders = options.addResponseHeaders !== false;
    const traceContextExtractor = options.traceContextExtractor ?? defaultTracingSecurityContextExtractor;
    const tracer = getTracer(VorionTracers.API);
    // Track active spans by request
    const activeSpans = new WeakMap();
    // onRequest hook - create span
    fastify.addHook('onRequest', async (request, reply) => {
        // Skip excluded paths
        const urlPath = request.url.split('?')[0];
        if (excludePaths.some((path) => urlPath.startsWith(path))) {
            return;
        }
        // Extract parent context from headers
        const parentContext = extractContextFromHeaders(request.headers);
        // Create span
        const spanName = getSpanName(request);
        const span = tracer.startSpan(spanName, {
            kind: SpanKind.SERVER,
            attributes: getHttpAttributes(request),
        }, parentContext);
        // Store span on request
        activeSpans.set(request, span);
        // Set trace context
        const tracedRequest = request;
        tracedRequest.span = span;
        tracedRequest.otelContext = trace.setSpan(parentContext, span);
        // Get span context
        const spanContext = span.spanContext();
        tracedRequest.traceId = spanContext.traceId;
        tracedRequest.spanId = spanContext.spanId;
        // Add trace headers to response
        if (addResponseHeaders) {
            reply.header('X-Trace-ID', spanContext.traceId);
            reply.header('X-Span-ID', spanContext.spanId);
        }
        // Call custom request hook
        options.requestHook?.(span, request);
    });
    // preHandler hook - extract security context
    fastify.addHook('preHandler', async (request, _reply) => {
        const span = activeSpans.get(request);
        if (!span) {
            return;
        }
        // Extract tracing security context
        const tracingCtx = traceContextExtractor(request);
        if (tracingCtx) {
            const tracedRequest = request;
            tracedRequest.tracingSecurityContext = tracingCtx;
            // Add security attributes to span
            if (tracingCtx.tenantId) {
                span.setAttribute(VorionSpanAttributes.TENANT_ID, tracingCtx.tenantId);
            }
            if (tracingCtx.entityId) {
                span.setAttribute(VorionSpanAttributes.ENTITY_ID, tracingCtx.entityId);
            }
            if (tracingCtx.userId) {
                span.setAttribute('user.id', tracingCtx.userId);
            }
            if (tracingCtx.roles?.length) {
                span.setAttribute('user.roles', tracingCtx.roles.join(','));
            }
            if (tracingCtx.sessionId) {
                span.setAttribute('session.id', tracingCtx.sessionId);
            }
            if (tracingCtx.apiKeyId) {
                span.setAttribute('api_key.id', tracingCtx.apiKeyId);
            }
        }
    });
    // onResponse hook - end span
    fastify.addHook('onResponse', async (request, reply) => {
        const span = activeSpans.get(request);
        if (!span) {
            return;
        }
        // Add response attributes
        span.setAttributes({
            'http.status_code': reply.statusCode,
            'http.response_content_length': reply.getHeader('content-length') ?? 0,
        });
        // Set span status based on HTTP status
        if (reply.statusCode >= 500) {
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: `HTTP ${reply.statusCode}`,
            });
        }
        else if (reply.statusCode >= 400) {
            // Client errors are not span errors
            span.setStatus({ code: SpanStatusCode.OK });
        }
        else {
            span.setStatus({ code: SpanStatusCode.OK });
        }
        // Call custom response hook
        options.responseHook?.(span, request, reply);
        // End span
        span.end();
        activeSpans.delete(request);
    });
    // onError hook - record error
    fastify.addHook('onError', async (request, _reply, error) => {
        const span = activeSpans.get(request);
        if (!span) {
            return;
        }
        // Record exception
        span.recordException(error);
        span.setStatus({
            code: SpanStatusCode.ERROR,
            message: error.message,
        });
        // Add error attributes
        span.setAttributes({
            'error': true,
            'error.type': error.name,
            'error.message': error.message,
        });
    });
    // Decorate Fastify with tracing utilities
    fastify.decorate('getActiveSpan', () => {
        return trace.getActiveSpan();
    });
    fastify.decorate('getTraceContext', () => {
        return getTraceContext();
    });
    fastify.decorate('injectTraceHeaders', (headers) => {
        return injectContextToHeaders(headers);
    });
    logger.info({ excludePaths, addResponseHeaders }, 'Tracing middleware registered');
    done();
};
/**
 * Fastify tracing plugin
 */
export const tracingMiddleware = fp(tracingMiddlewarePlugin, {
    name: 'vorion-tracing',
    fastify: '>=4.x',
});
/**
 * Create request-scoped tracing context
 *
 * Run async operations within the request's trace context
 */
export function runInRequestContext(request, fn) {
    const tracedRequest = request;
    if (!tracedRequest.otelContext) {
        return fn();
    }
    return context.with(tracedRequest.otelContext, fn);
}
/**
 * Create a child span for the current request
 */
export function createRequestChildSpan(request, name, attributes) {
    const tracedRequest = request;
    const tracer = getTracer(VorionTracers.API);
    return tracer.startSpan(name, {
        kind: SpanKind.INTERNAL,
        attributes,
    }, tracedRequest.otelContext);
}
/**
 * Execute a function within a child span of the request
 */
export async function withRequestSpan(request, name, fn, attributes) {
    const tracedRequest = request;
    const tracer = getTracer(VorionTracers.API);
    const ctx = tracedRequest.otelContext ?? context.active();
    return tracer.startActiveSpan(name, {
        kind: SpanKind.INTERNAL,
        attributes,
    }, ctx, async (span) => {
        try {
            const result = await fn(span);
            span.setStatus({ code: SpanStatusCode.OK });
            return result;
        }
        catch (error) {
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: error instanceof Error ? error.message : 'Unknown error',
            });
            if (error instanceof Error) {
                span.recordException(error);
            }
            throw error;
        }
        finally {
            span.end();
        }
    });
}
/**
 * Add attributes to the request's span
 */
export function addRequestSpanAttributes(request, attributes) {
    const tracedRequest = request;
    tracedRequest.span?.setAttributes(attributes);
}
/**
 * Add an event to the request's span
 */
export function addRequestSpanEvent(request, name, attributes) {
    const tracedRequest = request;
    tracedRequest.span?.addEvent(name, attributes);
}
/**
 * Record an error on the request's span
 */
export function recordRequestError(request, error, attributes) {
    const tracedRequest = request;
    if (!tracedRequest.span) {
        return;
    }
    tracedRequest.span.recordException(error);
    tracedRequest.span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error.message,
    });
    // Add additional context as event if provided
    if (attributes) {
        tracedRequest.span.addEvent('error.context', attributes);
    }
}
/**
 * Get trace ID from request
 */
export function getRequestTraceId(request) {
    return request.traceId;
}
/**
 * Get span ID from request
 */
export function getRequestSpanId(request) {
    return request.spanId;
}
/**
 * Get tracing security context from request
 */
export function getRequestTracingSecurityContext(request) {
    return request.tracingSecurityContext;
}
//# sourceMappingURL=middleware.js.map