// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
import { ZodError } from 'zod';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { createLogger } from '../common/logger.js';
import { getTraceContext } from '../common/trace.js';
import { getConfig } from '../common/config.js';
import { isVorionError } from '../common/errors.js';
const logger = createLogger({ component: 'api-errors' });
const tracer = trace.getTracer('vorion-api-errors');
/**
 * Standard error codes for API responses
 */
export var ErrorCode;
(function (ErrorCode) {
    // Client Errors (4xx)
    ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
    ErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
    ErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS";
    ErrorCode["INVALID_QUERY"] = "INVALID_QUERY";
    ErrorCode["MISSING_FIELD"] = "MISSING_FIELD";
    ErrorCode["INVALID_FORMAT"] = "INVALID_FORMAT";
    ErrorCode["PAYLOAD_TOO_LARGE"] = "PAYLOAD_TOO_LARGE";
    ErrorCode["INJECTION_DETECTED"] = "INJECTION_DETECTED";
    ErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
    ErrorCode["TOKEN_INVALID"] = "TOKEN_INVALID";
    ErrorCode["TOKEN_EXPIRED"] = "TOKEN_EXPIRED";
    ErrorCode["TOKEN_REVOKED"] = "TOKEN_REVOKED";
    ErrorCode["MISSING_AUTH"] = "MISSING_AUTH";
    ErrorCode["FORBIDDEN"] = "FORBIDDEN";
    ErrorCode["INSUFFICIENT_PERMISSIONS"] = "INSUFFICIENT_PERMISSIONS";
    ErrorCode["TENANT_MISMATCH"] = "TENANT_MISMATCH";
    ErrorCode["NOT_FOUND"] = "NOT_FOUND";
    ErrorCode["RESOURCE_NOT_FOUND"] = "RESOURCE_NOT_FOUND";
    ErrorCode["INTENT_NOT_FOUND"] = "INTENT_NOT_FOUND";
    ErrorCode["POLICY_NOT_FOUND"] = "POLICY_NOT_FOUND";
    ErrorCode["ESCALATION_NOT_FOUND"] = "ESCALATION_NOT_FOUND";
    ErrorCode["CONFLICT"] = "CONFLICT";
    ErrorCode["RESOURCE_EXISTS"] = "RESOURCE_EXISTS";
    ErrorCode["INVALID_STATE"] = "INVALID_STATE";
    ErrorCode["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
    ErrorCode["QUOTA_EXCEEDED"] = "QUOTA_EXCEEDED";
    // Server Errors (5xx)
    ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
    ErrorCode["DATABASE_ERROR"] = "DATABASE_ERROR";
    ErrorCode["EXTERNAL_SERVICE_ERROR"] = "EXTERNAL_SERVICE_ERROR";
    ErrorCode["TIMEOUT"] = "TIMEOUT";
    ErrorCode["SERVICE_UNAVAILABLE"] = "SERVICE_UNAVAILABLE";
})(ErrorCode || (ErrorCode = {}));
/**
 * HTTP status code mapping for error codes
 */
const ERROR_STATUS_MAP = {
    // 400 Bad Request
    [ErrorCode.VALIDATION_ERROR]: 400,
    [ErrorCode.INVALID_INPUT]: 400,
    [ErrorCode.INVALID_PARAMS]: 400,
    [ErrorCode.INVALID_QUERY]: 400,
    [ErrorCode.MISSING_FIELD]: 400,
    [ErrorCode.INVALID_FORMAT]: 400,
    [ErrorCode.INJECTION_DETECTED]: 400,
    // 401 Unauthorized
    [ErrorCode.UNAUTHORIZED]: 401,
    [ErrorCode.TOKEN_INVALID]: 401,
    [ErrorCode.TOKEN_EXPIRED]: 401,
    [ErrorCode.TOKEN_REVOKED]: 401,
    [ErrorCode.MISSING_AUTH]: 401,
    // 403 Forbidden
    [ErrorCode.FORBIDDEN]: 403,
    [ErrorCode.INSUFFICIENT_PERMISSIONS]: 403,
    [ErrorCode.TENANT_MISMATCH]: 403,
    // 404 Not Found
    [ErrorCode.NOT_FOUND]: 404,
    [ErrorCode.RESOURCE_NOT_FOUND]: 404,
    [ErrorCode.INTENT_NOT_FOUND]: 404,
    [ErrorCode.POLICY_NOT_FOUND]: 404,
    [ErrorCode.ESCALATION_NOT_FOUND]: 404,
    // 409 Conflict
    [ErrorCode.CONFLICT]: 409,
    [ErrorCode.RESOURCE_EXISTS]: 409,
    [ErrorCode.INVALID_STATE]: 409,
    // 413 Payload Too Large
    [ErrorCode.PAYLOAD_TOO_LARGE]: 413,
    // 429 Too Many Requests
    [ErrorCode.RATE_LIMIT_EXCEEDED]: 429,
    [ErrorCode.QUOTA_EXCEEDED]: 429,
    // 500 Internal Server Error
    [ErrorCode.INTERNAL_ERROR]: 500,
    [ErrorCode.DATABASE_ERROR]: 500,
    // 502 Bad Gateway
    [ErrorCode.EXTERNAL_SERVICE_ERROR]: 502,
    // 503 Service Unavailable
    [ErrorCode.SERVICE_UNAVAILABLE]: 503,
    // 504 Gateway Timeout
    [ErrorCode.TIMEOUT]: 504,
};
/**
 * Base API error class
 */
export class ApiError extends Error {
    code;
    statusCode;
    details;
    retryAfter;
    constructor(code, message, details, retryAfter) {
        super(message);
        this.name = 'ApiError';
        this.code = code;
        this.statusCode = ERROR_STATUS_MAP[code] ?? 500;
        this.details = details;
        this.retryAfter = retryAfter;
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, this.constructor);
        }
    }
    /**
     * Serialize to VorionErrorResponse format
     */
    toResponse(requestId, includeDetails = true) {
        const response = {
            success: false,
            error: {
                code: this.code,
                message: this.message,
            },
            meta: {
                requestId,
                timestamp: new Date().toISOString(),
            },
        };
        if (includeDetails && this.details) {
            response.error.details = this.details;
        }
        if (this.retryAfter !== undefined) {
            response.error.retryAfter = this.retryAfter;
        }
        return response;
    }
}
/**
 * Not Found error (404)
 */
export class NotFoundError extends ApiError {
    constructor(resource = 'Resource', message, details) {
        super(ErrorCode.NOT_FOUND, message ?? `${resource} not found`, details);
        this.name = 'NotFoundError';
    }
}
/**
 * Validation error (400)
 */
export class ValidationError extends ApiError {
    constructor(message = 'Validation failed', details) {
        super(ErrorCode.VALIDATION_ERROR, message, details);
        this.name = 'ValidationError';
    }
    /**
     * Create from Zod error
     */
    static fromZodError(error) {
        const errors = error.issues?.map ?? [].map((e) => ({
            path: e.path.join('.') || '(root)',
            message: e.message,
            code: e.code,
        }));
        return new ValidationError('Request validation failed', { errors });
    }
}
/**
 * Authentication error (401)
 */
export class AuthError extends ApiError {
    constructor(message = 'Authentication required', code = ErrorCode.UNAUTHORIZED, details) {
        super(code, message, details);
        this.name = 'AuthError';
    }
    static tokenInvalid(message = 'Invalid token') {
        return new AuthError(message, ErrorCode.TOKEN_INVALID);
    }
    static tokenExpired(message = 'Token has expired') {
        return new AuthError(message, ErrorCode.TOKEN_EXPIRED);
    }
    static tokenRevoked(message = 'Token has been revoked') {
        return new AuthError(message, ErrorCode.TOKEN_REVOKED);
    }
    static missingAuth(message = 'Authorization header is required') {
        return new AuthError(message, ErrorCode.MISSING_AUTH);
    }
}
/**
 * Rate limit error (429)
 */
export class RateLimitError extends ApiError {
    constructor(message = 'Rate limit exceeded', retryAfter, details) {
        super(ErrorCode.RATE_LIMIT_EXCEEDED, message, details, retryAfter);
        this.name = 'RateLimitError';
    }
}
/**
 * Conflict error (409)
 */
export class ConflictError extends ApiError {
    constructor(message = 'Resource conflict', details) {
        super(ErrorCode.CONFLICT, message, details);
        this.name = 'ConflictError';
    }
    static invalidState(currentState, allowedStates) {
        return new ConflictError(`Invalid state transition. Current state: ${currentState}, allowed states: ${allowedStates.join(', ')}`, { currentState, allowedStates });
    }
}
/**
 * Forbidden error (403)
 */
export class ForbiddenError extends ApiError {
    constructor(message = 'Access denied', details) {
        super(ErrorCode.FORBIDDEN, message, details);
        this.name = 'ForbiddenError';
    }
    static insufficientPermissions(requiredPermission) {
        return new ForbiddenError(`Insufficient permissions. Required: ${requiredPermission}`, { requiredPermission });
    }
    static tenantMismatch() {
        return new ForbiddenError('Access denied. Resource belongs to a different tenant.', {});
    }
}
/**
 * Internal server error (500)
 */
export class InternalError extends ApiError {
    constructor(message = 'An unexpected error occurred', details) {
        super(ErrorCode.INTERNAL_ERROR, message, details);
        this.name = 'InternalError';
    }
}
/**
 * Get status code for an error code
 */
export function getStatusForErrorCode(code) {
    return ERROR_STATUS_MAP[code] ?? 500;
}
/**
 * Check if an error is an API error
 */
export function isApiError(error) {
    return error instanceof ApiError;
}
/**
 * Sanitize error message for production
 * Removes potentially sensitive information
 */
function sanitizeErrorMessage(message) {
    const sensitivePatterns = [
        /password/i,
        /secret/i,
        /token/i,
        /key/i,
        /credential/i,
        /connection string/i,
        /database.*error/i,
        /sql.*error/i,
    ];
    for (const pattern of sensitivePatterns) {
        if (pattern.test(message)) {
            return 'An error occurred while processing your request';
        }
    }
    // Remove stack traces
    const stackIndex = message.indexOf('\n    at ');
    if (stackIndex !== -1) {
        return message.substring(0, stackIndex);
    }
    return message;
}
/**
 * Create error response from any error
 */
export function createErrorResponse(error, requestId, isProduction = false) {
    const traceContext = getTraceContext();
    // Handle API errors
    if (isApiError(error)) {
        const response = error.toResponse(requestId, !isProduction);
        if (traceContext) {
            response.trace = { traceId: traceContext.traceId };
        }
        return { response, statusCode: error.statusCode };
    }
    // Handle Vorion errors from common/errors.ts
    if (isVorionError(error)) {
        const response = {
            success: false,
            error: {
                code: error.code,
                message: isProduction ? sanitizeErrorMessage(error.message) : error.message,
                details: !isProduction && error.details ? error.details : undefined,
            },
            meta: {
                requestId,
                timestamp: new Date().toISOString(),
            },
        };
        if (traceContext) {
            response.trace = { traceId: traceContext.traceId };
        }
        return { response, statusCode: error.statusCode };
    }
    // Handle Zod validation errors (duck-type check for Zod v4 cross-module compat)
    if (error instanceof ZodError || (error && typeof error === 'object' && 'issues' in error && Array.isArray(error.issues) && error.name === 'ZodError')) {
        const validationError = ValidationError.fromZodError(error);
        const response = validationError.toResponse(requestId, !isProduction);
        if (traceContext) {
            response.trace = { traceId: traceContext.traceId };
        }
        return { response, statusCode: 400 };
    }
    // Handle Fastify errors
    if (isFastifyError(error)) {
        let code = ErrorCode.INTERNAL_ERROR;
        let statusCode = error.statusCode ?? 500;
        let message = error.message;
        // Map specific Fastify error codes
        if (error.code === 'FST_JWT_NO_AUTHORIZATION_IN_HEADER') {
            code = ErrorCode.MISSING_AUTH;
            statusCode = 401;
            message = 'Authorization header is missing';
        }
        else if (error.code === 'FST_JWT_AUTHORIZATION_TOKEN_INVALID') {
            code = ErrorCode.TOKEN_INVALID;
            statusCode = 401;
            message = 'Invalid authorization token';
        }
        else if (error.code === 'FST_JWT_AUTHORIZATION_TOKEN_EXPIRED') {
            code = ErrorCode.TOKEN_EXPIRED;
            statusCode = 401;
            message = 'Authorization token has expired';
        }
        else if (statusCode === 429) {
            code = ErrorCode.RATE_LIMIT_EXCEEDED;
        }
        else if (statusCode === 413) {
            code = ErrorCode.PAYLOAD_TOO_LARGE;
        }
        const response = {
            success: false,
            error: {
                code,
                message: isProduction && statusCode >= 500 ? 'An unexpected error occurred' : message,
            },
            meta: {
                requestId,
                timestamp: new Date().toISOString(),
            },
        };
        if (traceContext) {
            response.trace = { traceId: traceContext.traceId };
        }
        return { response, statusCode };
    }
    // Handle generic errors
    const message = error instanceof Error ? error.message : 'Unknown error';
    const response = {
        success: false,
        error: {
            code: ErrorCode.INTERNAL_ERROR,
            message: isProduction ? 'An unexpected error occurred' : sanitizeErrorMessage(message),
        },
        meta: {
            requestId,
            timestamp: new Date().toISOString(),
        },
    };
    if (traceContext) {
        response.trace = { traceId: traceContext.traceId };
    }
    return { response, statusCode: 500 };
}
/**
 * Type guard for Fastify errors
 */
function isFastifyError(error) {
    return (error !== null &&
        typeof error === 'object' &&
        'statusCode' in error &&
        typeof error.statusCode === 'number');
}
/**
 * Create standard error handler middleware for Fastify
 *
 * @param options - Error handler options
 * @returns Fastify error handler
 */
export function createErrorHandler(options) {
    const config = getConfig();
    const isProduction = config.env === 'production';
    return async (error, request, reply) => {
        const requestId = request.id || 'unknown';
        const traceContext = request.traceContext;
        // Apply custom transform if provided
        const transformedError = options?.transformError?.(error) ?? error;
        // Create error response
        const { response, statusCode } = createErrorResponse(transformedError, requestId, isProduction);
        // Add trace context if available
        if (traceContext && response.trace) {
            response.trace.traceId = traceContext.traceId;
        }
        // Log the error
        const logData = {
            requestId,
            statusCode,
            errorCode: response.error.code,
            url: request.url,
            method: request.method,
        };
        if (traceContext) {
            logData.traceId = traceContext.traceId;
        }
        if (!isProduction && options?.includeStack !== false) {
            logData.stack = error.stack;
        }
        // Log level based on status code
        if (statusCode >= 500) {
            logger.error(logData, error.message);
            // Record exception in trace
            const span = trace.getActiveSpan();
            if (span) {
                span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
                span.recordException(error);
            }
        }
        else if (statusCode >= 400) {
            logger.warn(logData, error.message);
        }
        else {
            logger.info(logData, error.message);
        }
        // Add Retry-After header for rate limit errors
        if (response.error.retryAfter !== undefined) {
            reply.header('Retry-After', response.error.retryAfter);
        }
        return reply.status(statusCode).send(response);
    };
}
/**
 * Wrap an async handler to catch and format errors
 */
export function wrapHandler(handler) {
    return async (request, reply) => {
        try {
            return await handler(request, reply);
        }
        catch (error) {
            const config = getConfig();
            const { response, statusCode } = createErrorResponse(error, request.id || 'unknown', config.env === 'production');
            reply.status(statusCode);
            return response;
        }
    };
}
//# sourceMappingURL=errors.js.map