/**
 * API Input Validation Middleware
 *
 * Provides Zod-based validation middleware for Fastify routes with
 * OpenTelemetry tracing, detailed error responses, and security features.
 *
 * @packageDocumentation
 */
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
import { createLogger } from '../../common/logger.js';
import { getTraceContext } from '../../common/trace.js';
import { sanitizeObject, checkInjectionPatterns, validatePayloadSize, ValidationError as CommonValidationError, } from '../../common/validation.js';
const logger = createLogger({ component: 'api-validation' });
const tracer = trace.getTracer('vorion-api-validation');
/**
 * Default validation options
 */
const DEFAULT_OPTIONS = {
    maxPayloadSize: 1048576, // 1MB
    checkInjection: true,
    sanitize: true,
    stripUnknown: true,
};
/**
 * Format Zod errors into detailed validation error response
 */
function formatZodErrors(error) {
    return error.errors.map((err) => {
        const detail = {
            path: err.path.join('.') || '(root)',
            message: err.message,
            code: err.code,
        };
        // Add type information for type errors
        if (err.code === 'invalid_type') {
            const typeErr = err;
            detail.expected = typeErr.expected;
            detail.received = typeErr.received;
        }
        return detail;
    });
}
/**
 * Create validation error response conforming to VorionErrorResponse
 */
function createValidationErrorResponse(errors, requestId, customMessage) {
    return {
        success: false,
        error: {
            code: 'VALIDATION_ERROR',
            message: customMessage ?? 'Request validation failed',
            details: { errors },
        },
        meta: {
            requestId,
            timestamp: new Date().toISOString(),
        },
    };
}
/**
 * Recursively check object for injection patterns
 */
function checkObjectForInjection(obj, path = '') {
    for (const [key, value] of Object.entries(obj)) {
        const fieldPath = path ? `${path}.${key}` : key;
        if (typeof value === 'string' && checkInjectionPatterns(value)) {
            return fieldPath;
        }
        if (Array.isArray(value)) {
            for (let i = 0; i < value.length; i++) {
                const item = value[i];
                if (typeof item === 'string' && checkInjectionPatterns(item)) {
                    return `${fieldPath}[${i}]`;
                }
                if (typeof item === 'object' && item !== null) {
                    const result = checkObjectForInjection(item, `${fieldPath}[${i}]`);
                    if (result)
                        return result;
                }
            }
        }
        if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
            const result = checkObjectForInjection(value, fieldPath);
            if (result)
                return result;
        }
    }
    return null;
}
/**
 * Create validation middleware for request body
 *
 * @param schema - Zod schema to validate against
 * @param options - Validation options
 * @returns Fastify preHandler hook
 *
 * @example
 * ```typescript
 * const createUserSchema = z.object({
 *   name: z.string().min(1).max(100),
 *   email: z.string().email(),
 * });
 *
 * server.post('/users', {
 *   preHandler: validateBody(createUserSchema),
 * }, handler);
 * ```
 */
export function validateBody(schema, options = {}) {
    const opts = { ...DEFAULT_OPTIONS, ...options };
    return async (request, reply) => {
        const requestId = request.id || 'unknown';
        const traceContext = getTraceContext();
        return tracer.startActiveSpan('validation.body', {
            kind: SpanKind.INTERNAL,
            attributes: {
                'validation.type': 'body',
                'request.id': requestId,
                'request.path': request.url,
                ...(traceContext && { 'trace.id': traceContext.traceId }),
            },
        }, async (span) => {
            try {
                const body = request.body;
                // Check if body exists
                if (body === undefined || body === null) {
                    span.setStatus({ code: SpanStatusCode.ERROR, message: 'Missing request body' });
                    span.end();
                    const response = createValidationErrorResponse([{ path: '(root)', message: 'Request body is required', code: 'missing_body' }], requestId, opts.customErrorMessage);
                    reply.status(400).send(response);
                    return;
                }
                // Check payload size
                if (opts.maxPayloadSize) {
                    try {
                        validatePayloadSize(body, opts.maxPayloadSize);
                    }
                    catch (error) {
                        span.setStatus({ code: SpanStatusCode.ERROR, message: 'Payload too large' });
                        span.setAttribute('validation.error', 'payload_too_large');
                        span.end();
                        const response = createValidationErrorResponse([{
                                path: '(root)',
                                message: `Payload size exceeds maximum of ${opts.maxPayloadSize} bytes`,
                                code: 'payload_too_large',
                            }], requestId, 'Request payload too large');
                        reply.status(413).send(response);
                        return;
                    }
                }
                // Sanitize if enabled
                let sanitized = body;
                if (opts.sanitize && typeof body === 'object' && body !== null) {
                    sanitized = sanitizeObject(body);
                    span.setAttribute('validation.sanitized', true);
                }
                // Check for injection patterns
                if (opts.checkInjection && typeof sanitized === 'object' && sanitized !== null) {
                    const injectionPath = checkObjectForInjection(sanitized);
                    if (injectionPath) {
                        span.setStatus({ code: SpanStatusCode.ERROR, message: 'Injection detected' });
                        span.setAttribute('validation.error', 'injection_detected');
                        span.setAttribute('validation.injection_path', injectionPath);
                        span.end();
                        logger.warn({ requestId, path: injectionPath, url: request.url }, 'Potential injection attack detected');
                        const response = createValidationErrorResponse([{
                                path: injectionPath,
                                message: 'Potentially dangerous pattern detected',
                                code: 'injection_detected',
                            }], requestId, 'Request contains potentially dangerous content');
                        reply.status(400).send(response);
                        return;
                    }
                }
                // Validate with schema
                const result = schema.safeParse(sanitized);
                if (!result.success) {
                    const errors = formatZodErrors(result.error);
                    span.setStatus({ code: SpanStatusCode.ERROR, message: 'Validation failed' });
                    span.setAttribute('validation.error_count', errors.length);
                    span.end();
                    logger.warn({ requestId, errors, path: request.url }, 'Request body validation failed');
                    const response = createValidationErrorResponse(errors, requestId, opts.customErrorMessage);
                    reply.status(400).send(response);
                    return;
                }
                // Replace body with validated/sanitized version
                request.body = result.data;
                span.setStatus({ code: SpanStatusCode.OK });
                span.setAttribute('validation.success', true);
                span.end();
            }
            catch (error) {
                span.setStatus({ code: SpanStatusCode.ERROR, message: 'Validation error' });
                span.recordException(error);
                span.end();
                if (error instanceof CommonValidationError) {
                    const response = createValidationErrorResponse([{ path: error.field ?? '(unknown)', message: error.message, code: error.code }], requestId);
                    reply.status(400).send(response);
                    return;
                }
                throw error;
            }
        });
    };
}
/**
 * Create validation middleware for query parameters
 *
 * @param schema - Zod schema to validate against
 * @returns Fastify preHandler hook
 *
 * @example
 * ```typescript
 * const listQuerySchema = z.object({
 *   page: z.coerce.number().int().min(1).default(1),
 *   limit: z.coerce.number().int().min(1).max(100).default(20),
 * });
 *
 * server.get('/users', {
 *   preHandler: validateQuery(listQuerySchema),
 * }, handler);
 * ```
 */
export function validateQuery(schema) {
    return async (request, reply) => {
        const requestId = request.id || 'unknown';
        return tracer.startActiveSpan('validation.query', {
            kind: SpanKind.INTERNAL,
            attributes: {
                'validation.type': 'query',
                'request.id': requestId,
                'request.path': request.url,
            },
        }, async (span) => {
            const result = schema.safeParse(request.query);
            if (!result.success) {
                const errors = formatZodErrors(result.error);
                span.setStatus({ code: SpanStatusCode.ERROR, message: 'Query validation failed' });
                span.setAttribute('validation.error_count', errors.length);
                span.end();
                logger.warn({ requestId, errors, path: request.url }, 'Query parameter validation failed');
                const response = createValidationErrorResponse(errors, requestId, 'Invalid query parameters');
                reply.status(400).send(response);
                return;
            }
            // Replace query with validated version
            request.query = result.data;
            span.setStatus({ code: SpanStatusCode.OK });
            span.setAttribute('validation.success', true);
            span.end();
        });
    };
}
/**
 * Create validation middleware for path parameters
 *
 * @param schema - Zod schema to validate against
 * @returns Fastify preHandler hook
 *
 * @example
 * ```typescript
 * const idParamSchema = z.object({
 *   id: z.string().uuid(),
 * });
 *
 * server.get('/users/:id', {
 *   preHandler: validateParams(idParamSchema),
 * }, handler);
 * ```
 */
export function validateParams(schema) {
    return async (request, reply) => {
        const requestId = request.id || 'unknown';
        return tracer.startActiveSpan('validation.params', {
            kind: SpanKind.INTERNAL,
            attributes: {
                'validation.type': 'params',
                'request.id': requestId,
                'request.path': request.url,
            },
        }, async (span) => {
            const result = schema.safeParse(request.params);
            if (!result.success) {
                const errors = formatZodErrors(result.error);
                span.setStatus({ code: SpanStatusCode.ERROR, message: 'Params validation failed' });
                span.setAttribute('validation.error_count', errors.length);
                span.end();
                logger.warn({ requestId, errors, path: request.url }, 'Path parameter validation failed');
                const response = createValidationErrorResponse(errors, requestId, 'Invalid path parameters');
                reply.status(400).send(response);
                return;
            }
            // Replace params with validated version
            request.params = result.data;
            span.setStatus({ code: SpanStatusCode.OK });
            span.setAttribute('validation.success', true);
            span.end();
        });
    };
}
/**
 * Create combined validation middleware for body, query, and params
 *
 * @param schemas - Object containing schemas for body, query, and/or params
 * @param options - Validation options (applies to body only)
 * @returns Fastify preHandler hook
 *
 * @example
 * ```typescript
 * server.put('/users/:id', {
 *   preHandler: validateRequest({
 *     params: z.object({ id: z.string().uuid() }),
 *     body: z.object({ name: z.string() }),
 *   }),
 * }, handler);
 * ```
 */
export function validateRequest(schemas, options = {}) {
    const validators = [];
    if (schemas.params) {
        validators.push(validateParams(schemas.params));
    }
    if (schemas.query) {
        validators.push(validateQuery(schemas.query));
    }
    if (schemas.body) {
        validators.push(validateBody(schemas.body, options));
    }
    return async (request, reply) => {
        for (const validator of validators) {
            await validator(request, reply);
            // Stop if a response was already sent (validation failed)
            if (reply.sent) {
                return;
            }
        }
    };
}
/**
 * Register validation middleware plugin for Fastify
 *
 * This plugin adds the validation decorators to the Fastify instance.
 *
 * @param server - Fastify instance
 * @param options - Default validation options
 */
export async function registerValidationPlugin(server, options = {}) {
    // Merge with defaults
    const defaultOpts = { ...DEFAULT_OPTIONS, ...options };
    // Create factory functions for validation middleware
    const createBodyValidator = (schema, opts) => validateBody(schema, { ...defaultOpts, ...opts });
    const createQueryValidator = (schema) => validateQuery(schema);
    const createParamsValidator = (schema) => validateParams(schema);
    const createRequestValidator = (schemas, opts) => validateRequest(schemas, { ...defaultOpts, ...opts });
    // Decorate server with validation helpers
    server.decorate('validateBody', createBodyValidator);
    server.decorate('validateQuery', createQueryValidator);
    server.decorate('validateParams', createParamsValidator);
    server.decorate('validateRequest', createRequestValidator);
    logger.info({ component: 'api-validation' }, 'Validation plugin registered');
}
//# sourceMappingURL=validation.js.map