Source: zpt/api/ErrorHandler.js

/**
 * Comprehensive error handling for ZPT navigation API
 */
import { logger } from '../../Utils.js';

export default class ErrorHandler {
    constructor(options = {}) {
        this.config = {
            logErrors: options.logErrors !== false,
            includeStackTrace: options.includeStackTrace || false,
            enableErrorReporting: options.enableErrorReporting || false,
            sanitizeErrorMessages: options.sanitizeErrorMessages !== false,
            maxErrorMessageLength: options.maxErrorMessageLength || 500,
            enableRecovery: options.enableRecovery !== false,
            ...options
        };

        this.initializeErrorTypes();
        this.initializeErrorCodes();
        this.initializeRecoveryStrategies();
        
        // Error statistics
        this.errorStats = {
            totalErrors: 0,
            errorsByType: new Map(),
            errorsByCode: new Map(),
            recentErrors: []
        };
    }

    /**
     * Initialize error type definitions
     */
    initializeErrorTypes() {
        this.errorTypes = {
            VALIDATION_ERROR: {
                statusCode: 400,
                category: 'client',
                severity: 'medium',
                recoverable: true,
                description: 'Request validation failed'
            },
            AUTHENTICATION_ERROR: {
                statusCode: 401,
                category: 'client',
                severity: 'medium',
                recoverable: false,
                description: 'Authentication required or failed'
            },
            AUTHORIZATION_ERROR: {
                statusCode: 403,
                category: 'client',
                severity: 'medium',
                recoverable: false,
                description: 'Insufficient permissions'
            },
            NOT_FOUND_ERROR: {
                statusCode: 404,
                category: 'client',
                severity: 'low',
                recoverable: false,
                description: 'Resource not found'
            },
            RATE_LIMIT_ERROR: {
                statusCode: 429,
                category: 'client',
                severity: 'medium',
                recoverable: true,
                description: 'Rate limit exceeded'
            },
            PARAMETER_ERROR: {
                statusCode: 400,
                category: 'client',
                severity: 'medium',
                recoverable: true,
                description: 'Invalid navigation parameters'
            },
            CORPUS_ERROR: {
                statusCode: 503,
                category: 'server',
                severity: 'high',
                recoverable: true,
                description: 'Corpus access error'
            },
            PROCESSING_ERROR: {
                statusCode: 500,
                category: 'server',
                severity: 'high',
                recoverable: true,
                description: 'Navigation processing failed'
            },
            TIMEOUT_ERROR: {
                statusCode: 408,
                category: 'server',
                severity: 'medium',
                recoverable: true,
                description: 'Request timeout'
            },
            RESOURCE_ERROR: {
                statusCode: 507,
                category: 'server',
                severity: 'high',
                recoverable: false,
                description: 'Insufficient resources'
            },
            CONFIGURATION_ERROR: {
                statusCode: 500,
                category: 'server',
                severity: 'critical',
                recoverable: false,
                description: 'System configuration error'
            },
            EXTERNAL_SERVICE_ERROR: {
                statusCode: 502,
                category: 'server',
                severity: 'high',
                recoverable: true,
                description: 'External service unavailable'
            },
            UNKNOWN_ERROR: {
                statusCode: 500,
                category: 'server',
                severity: 'high',
                recoverable: false,
                description: 'Unknown system error'
            }
        };
    }

    /**
     * Initialize specific error codes
     */
    initializeErrorCodes() {
        this.errorCodes = {
            // Validation errors (4000-4099)
            'INVALID_ZOOM_LEVEL': { type: 'VALIDATION_ERROR', code: 4001 },
            'INVALID_TILT_REPRESENTATION': { type: 'VALIDATION_ERROR', code: 4002 },
            'INVALID_PAN_FILTER': { type: 'VALIDATION_ERROR', code: 4003 },
            'INVALID_TRANSFORM_PARAMS': { type: 'VALIDATION_ERROR', code: 4004 },
            'MISSING_REQUIRED_PARAM': { type: 'VALIDATION_ERROR', code: 4005 },
            'INVALID_TOKEN_LIMIT': { type: 'VALIDATION_ERROR', code: 4006 },
            'INVALID_FORMAT': { type: 'VALIDATION_ERROR', code: 4007 },
            'INVALID_DATE_RANGE': { type: 'VALIDATION_ERROR', code: 4008 },
            'INVALID_GEOGRAPHIC_BOUNDS': { type: 'VALIDATION_ERROR', code: 4009 },
            
            // Parameter errors (4100-4199)
            'PARAMETER_VALIDATION_FAILED': { type: 'PARAMETER_ERROR', code: 4101 },
            'PARAMETER_NORMALIZATION_FAILED': { type: 'PARAMETER_ERROR', code: 4102 },
            'INCOMPATIBLE_PARAMETERS': { type: 'PARAMETER_ERROR', code: 4103 },
            'PARAMETER_LIMIT_EXCEEDED': { type: 'PARAMETER_ERROR', code: 4104 },
            
            // Corpus errors (5000-5099)
            'CORPUS_UNAVAILABLE': { type: 'CORPUS_ERROR', code: 5001 },
            'CORPUS_QUERY_FAILED': { type: 'CORPUS_ERROR', code: 5002 },
            'CORPUS_TIMEOUT': { type: 'CORPUS_ERROR', code: 5003 },
            'SPARQL_ENDPOINT_ERROR': { type: 'CORPUS_ERROR', code: 5004 },
            'EMBEDDING_SERVICE_ERROR': { type: 'CORPUS_ERROR', code: 5005 },
            
            // Processing errors (5100-5199)
            'SELECTION_FAILED': { type: 'PROCESSING_ERROR', code: 5101 },
            'PROJECTION_FAILED': { type: 'PROCESSING_ERROR', code: 5102 },
            'TRANSFORMATION_FAILED': { type: 'PROCESSING_ERROR', code: 5103 },
            'CHUNKING_FAILED': { type: 'PROCESSING_ERROR', code: 5104 },
            'FORMATTING_FAILED': { type: 'PROCESSING_ERROR', code: 5105 },
            'ENCODING_FAILED': { type: 'PROCESSING_ERROR', code: 5106 },
            'TOKEN_COUNT_EXCEEDED': { type: 'PROCESSING_ERROR', code: 5107 },
            
            // Resource errors (5200-5299)
            'MEMORY_LIMIT_EXCEEDED': { type: 'RESOURCE_ERROR', code: 5201 },
            'CPU_LIMIT_EXCEEDED': { type: 'RESOURCE_ERROR', code: 5202 },
            'DISK_SPACE_ERROR': { type: 'RESOURCE_ERROR', code: 5203 },
            'CONNECTION_LIMIT_EXCEEDED': { type: 'RESOURCE_ERROR', code: 5204 },
            
            // Configuration errors (5300-5399)
            'MISSING_DEPENDENCY': { type: 'CONFIGURATION_ERROR', code: 5301 },
            'INVALID_CONFIGURATION': { type: 'CONFIGURATION_ERROR', code: 5302 },
            'SERVICE_NOT_INITIALIZED': { type: 'CONFIGURATION_ERROR', code: 5303 },
            
            // External service errors (5400-5499)
            'LLM_SERVICE_UNAVAILABLE': { type: 'EXTERNAL_SERVICE_ERROR', code: 5401 },
            'EMBEDDING_SERVICE_UNAVAILABLE': { type: 'EXTERNAL_SERVICE_ERROR', code: 5402 },
            'SPARQL_SERVICE_UNAVAILABLE': { type: 'EXTERNAL_SERVICE_ERROR', code: 5403 }
        };
    }

    /**
     * Initialize error recovery strategies
     */
    initializeRecoveryStrategies() {
        this.recoveryStrategies = {
            'VALIDATION_ERROR': this.recoverFromValidationError.bind(this),
            'PARAMETER_ERROR': this.recoverFromParameterError.bind(this),
            'CORPUS_ERROR': this.recoverFromCorpusError.bind(this),
            'PROCESSING_ERROR': this.recoverFromProcessingError.bind(this),
            'TIMEOUT_ERROR': this.recoverFromTimeoutError.bind(this),
            'EXTERNAL_SERVICE_ERROR': this.recoverFromExternalServiceError.bind(this)
        };
    }

    /**
     * Main error handling method
     * @param {Error} error - The error to handle
     * @param {Object} req - HTTP request object
     * @param {Object} res - HTTP response object
     * @param {Object} context - Additional context
     */
    async handleError(error, req, res, context = {}) {
        const errorId = this.generateErrorId();
        const timestamp = new Date().toISOString();

        try {
            // Normalize error
            const normalizedError = this.normalizeError(error, context);
            
            // Log error
            if (this.config.logErrors) {
                this.logError(normalizedError, errorId, req, context);
            }
            
            // Update statistics
            this.updateErrorStats(normalizedError);
            
            // Attempt recovery if enabled
            let recoveredError = normalizedError;
            if (this.config.enableRecovery && normalizedError.recoverable) {
                recoveredError = await this.attemptRecovery(normalizedError, req, context);
            }
            
            // Format error response
            const errorResponse = this.formatErrorResponse(recoveredError, errorId, context);
            
            // Send response
            this.sendErrorResponse(res, errorResponse, recoveredError.statusCode);
            
            // Report error if enabled
            if (this.config.enableErrorReporting) {
                await this.reportError(recoveredError, errorId, req, context);
            }

        } catch (handlingError) {
            // Fallback error handling
            logger.error('Error handler failed', { 
                originalError: error.message,
                handlingError: handlingError.message,
                errorId 
            });
            
            this.sendFallbackErrorResponse(res, errorId);
        }
    }

    /**
     * Normalize error into standard format
     */
    normalizeError(error, context) {
        // Determine error type and code
        const { errorType, errorCode } = this.classifyError(error, context);
        const typeInfo = this.errorTypes[errorType];
        
        return {
            id: this.generateErrorId(),
            type: errorType,
            code: errorCode,
            message: this.sanitizeErrorMessage(error.message),
            originalMessage: error.message,
            statusCode: context.statusCode || typeInfo.statusCode,
            category: typeInfo.category,
            severity: typeInfo.severity,
            recoverable: typeInfo.recoverable,
            timestamp: new Date().toISOString(),
            stackTrace: this.config.includeStackTrace ? error.stack : undefined,
            context: this.sanitizeContext(context),
            metadata: {
                field: error.field,
                details: error.details,
                cause: error.cause?.message
            }
        };
    }

    /**
     * Classify error type based on error properties
     */
    classifyError(error, context) {
        // Check for explicit error codes
        if (error.code && this.errorCodes[error.code]) {
            const codeInfo = this.errorCodes[error.code];
            return {
                errorType: codeInfo.type,
                errorCode: error.code
            };
        }

        // Check for status code override
        if (context.statusCode) {
            if (context.statusCode === 404) return { errorType: 'NOT_FOUND_ERROR', errorCode: 'NOT_FOUND' };
            if (context.statusCode === 429) return { errorType: 'RATE_LIMIT_ERROR', errorCode: 'RATE_LIMIT_EXCEEDED' };
        }

        // Pattern matching on error message
        const message = error.message.toLowerCase();
        
        if (message.includes('validation') || message.includes('invalid')) {
            return { errorType: 'VALIDATION_ERROR', errorCode: 'VALIDATION_FAILED' };
        }
        
        if (message.includes('parameter')) {
            return { errorType: 'PARAMETER_ERROR', errorCode: 'PARAMETER_ERROR' };
        }
        
        if (message.includes('timeout') || message.includes('timed out')) {
            return { errorType: 'TIMEOUT_ERROR', errorCode: 'REQUEST_TIMEOUT' };
        }
        
        if (message.includes('corpus') || message.includes('sparql')) {
            return { errorType: 'CORPUS_ERROR', errorCode: 'CORPUS_ACCESS_ERROR' };
        }
        
        if (message.includes('processing') || message.includes('transformation')) {
            return { errorType: 'PROCESSING_ERROR', errorCode: 'PROCESSING_FAILED' };
        }
        
        if (message.includes('memory') || message.includes('resource')) {
            return { errorType: 'RESOURCE_ERROR', errorCode: 'RESOURCE_EXHAUSTED' };
        }
        
        if (message.includes('service') || message.includes('unavailable')) {
            return { errorType: 'EXTERNAL_SERVICE_ERROR', errorCode: 'SERVICE_UNAVAILABLE' };
        }

        // Default classification
        return { errorType: 'UNKNOWN_ERROR', errorCode: 'UNKNOWN' };
    }

    /**
     * Attempt error recovery
     */
    async attemptRecovery(normalizedError, req, context) {
        const recoveryStrategy = this.recoveryStrategies[normalizedError.type];
        
        if (!recoveryStrategy) {
            return normalizedError;
        }

        try {
            const recoveryResult = await recoveryStrategy(normalizedError, req, context);
            
            if (recoveryResult.recovered) {
                return {
                    ...normalizedError,
                    recovered: true,
                    recoveryMessage: recoveryResult.message,
                    fallbackData: recoveryResult.data
                };
            }
        } catch (recoveryError) {
            logger.warn('Error recovery failed', { 
                originalError: normalizedError.code,
                recoveryError: recoveryError.message 
            });
        }

        return normalizedError;
    }

    /**
     * Recovery strategy implementations
     */
    async recoverFromValidationError(error, req, context) {
        // Try to provide corrected parameters
        if (error.code === 'INVALID_ZOOM_LEVEL') {
            return {
                recovered: true,
                message: 'Suggested valid zoom levels',
                data: {
                    validZoomLevels: ['entity', 'unit', 'text', 'community', 'corpus'],
                    suggestion: 'unit'
                }
            };
        }

        if (error.code === 'INVALID_TILT_REPRESENTATION') {
            return {
                recovered: true,
                message: 'Suggested valid tilt representations',
                data: {
                    validTilts: ['embedding', 'keywords', 'graph', 'temporal'],
                    suggestion: 'keywords'
                }
            };
        }

        return { recovered: false };
    }

    async recoverFromParameterError(error, req, context) {
        // Try to provide default parameters
        return {
            recovered: true,
            message: 'Using default parameters',
            data: {
                defaultParams: {
                    zoom: 'unit',
                    tilt: 'keywords',
                    transform: { maxTokens: 4000, format: 'structured' }
                }
            }
        };
    }

    async recoverFromCorpusError(error, req, context) {
        // Try alternative data sources or cached results
        return {
            recovered: true,
            message: 'Using cached corpus data',
            data: {
                source: 'cache',
                freshness: 'stale',
                limitation: 'Limited results available from cache'
            }
        };
    }

    async recoverFromProcessingError(error, req, context) {
        // Try simplified processing
        return {
            recovered: true,
            message: 'Using simplified processing mode',
            data: {
                mode: 'simplified',
                limitations: ['No chunking', 'Basic formatting', 'Reduced metadata']
            }
        };
    }

    async recoverFromTimeoutError(error, req, context) {
        // Suggest pagination or reduced scope
        return {
            recovered: true,
            message: 'Request timeout - suggest reducing scope',
            data: {
                suggestions: [
                    'Reduce maxTokens parameter',
                    'Use more specific pan filters',
                    'Try pagination for large results'
                ]
            }
        };
    }

    async recoverFromExternalServiceError(error, req, context) {
        // Try fallback services
        return {
            recovered: true,
            message: 'Using fallback processing mode',
            data: {
                mode: 'fallback',
                limitations: ['No embeddings', 'Basic text processing only']
            }
        };
    }

    /**
     * Format error response
     */
    formatErrorResponse(normalizedError, errorId, context) {
        const response = {
            success: false,
            error: {
                id: errorId,
                code: normalizedError.code,
                type: normalizedError.type,
                message: normalizedError.message,
                timestamp: normalizedError.timestamp,
                statusCode: normalizedError.statusCode
            },
            requestId: context.requestId
        };

        // Add recovery information
        if (normalizedError.recovered) {
            response.recovery = {
                recovered: true,
                message: normalizedError.recoveryMessage,
                fallbackData: normalizedError.fallbackData
            };
        }

        // Add field-specific information
        if (normalizedError.metadata.field) {
            response.error.field = normalizedError.metadata.field;
        }

        // Add details in debug mode
        if (this.config.includeStackTrace && normalizedError.stackTrace) {
            response.error.stackTrace = normalizedError.stackTrace;
        }

        // Add suggestions based on error type
        response.suggestions = this.generateErrorSuggestions(normalizedError);

        // Add rate limit information
        if (context.rateLimitInfo) {
            response.rateLimit = context.rateLimitInfo;
        }

        return response;
    }

    /**
     * Generate helpful suggestions based on error type
     */
    generateErrorSuggestions(error) {
        const suggestions = [];

        switch (error.type) {
            case 'VALIDATION_ERROR':
                suggestions.push('Check parameter format against API schema');
                suggestions.push('Use /api/navigate/schema endpoint for parameter documentation');
                break;

            case 'PARAMETER_ERROR':
                suggestions.push('Verify zoom/pan/tilt parameter combinations');
                suggestions.push('Use /api/navigate/options endpoint for valid values');
                break;

            case 'RATE_LIMIT_ERROR':
                suggestions.push('Reduce request frequency');
                suggestions.push('Implement exponential backoff');
                break;

            case 'TIMEOUT_ERROR':
                suggestions.push('Reduce scope of request (smaller maxTokens)');
                suggestions.push('Use more specific filters to narrow results');
                break;

            case 'CORPUS_ERROR':
                suggestions.push('Check system status');
                suggestions.push('Try again in a few moments');
                break;

            case 'PROCESSING_ERROR':
                suggestions.push('Simplify navigation parameters');
                suggestions.push('Try smaller token limits');
                break;
        }

        return suggestions;
    }

    /**
     * Utility methods
     */
    sanitizeErrorMessage(message) {
        if (!this.config.sanitizeErrorMessages) return message;
        
        // Remove potentially sensitive information
        let sanitized = message.replace(/password[=:]\s*\S+/gi, 'password=***');
        sanitized = sanitized.replace(/token[=:]\s*\S+/gi, 'token=***');
        sanitized = sanitized.replace(/key[=:]\s*\S+/gi, 'key=***');
        
        // Truncate if too long
        if (sanitized.length > this.config.maxErrorMessageLength) {
            sanitized = sanitized.substring(0, this.config.maxErrorMessageLength) + '...';
        }
        
        return sanitized;
    }

    sanitizeContext(context) {
        const sanitized = { ...context };
        
        // Remove sensitive fields
        delete sanitized.password;
        delete sanitized.token;
        delete sanitized.key;
        delete sanitized.authorization;
        
        return sanitized;
    }

    logError(error, errorId, req, context) {
        const logData = {
            errorId,
            type: error.type,
            code: error.code,
            message: error.message,
            severity: error.severity,
            statusCode: error.statusCode,
            requestId: context.requestId,
            method: req?.method,
            path: req?.url,
            userAgent: req?.headers?.['user-agent'],
            timestamp: error.timestamp
        };

        switch (error.severity) {
            case 'critical':
                logger.error('Critical error occurred', logData);
                break;
            case 'high':
                logger.error('High severity error', logData);
                break;
            case 'medium':
                logger.warn('Medium severity error', logData);
                break;
            case 'low':
                logger.info('Low severity error', logData);
                break;
            default:
                logger.error('Error occurred', logData);
        }
    }

    updateErrorStats(error) {
        this.errorStats.totalErrors++;
        
        // Update error type counts
        const typeCount = this.errorStats.errorsByType.get(error.type) || 0;
        this.errorStats.errorsByType.set(error.type, typeCount + 1);
        
        // Update error code counts
        const codeCount = this.errorStats.errorsByCode.get(error.code) || 0;
        this.errorStats.errorsByCode.set(error.code, codeCount + 1);
        
        // Keep recent errors (last 100)
        this.errorStats.recentErrors.push({
            id: error.id,
            type: error.type,
            code: error.code,
            timestamp: error.timestamp
        });
        
        if (this.errorStats.recentErrors.length > 100) {
            this.errorStats.recentErrors.shift();
        }
    }

    sendErrorResponse(res, errorResponse, statusCode) {
        res.setHeader('Content-Type', 'application/json');
        res.statusCode = statusCode || 500;
        res.end(JSON.stringify(errorResponse, null, 2));
    }

    sendFallbackErrorResponse(res, errorId) {
        const fallbackResponse = {
            success: false,
            error: {
                id: errorId,
                code: 'SYSTEM_ERROR',
                message: 'An unexpected system error occurred',
                timestamp: new Date().toISOString()
            }
        };
        
        res.setHeader('Content-Type', 'application/json');
        res.statusCode = 500;
        res.end(JSON.stringify(fallbackResponse));
    }

    generateErrorId() {
        return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }

    async reportError(error, errorId, req, context) {
        // Placeholder for external error reporting
        // Could integrate with services like Sentry, Rollbar, etc.
        logger.info('Error reported', { errorId, type: error.type, code: error.code });
    }

    /**
     * Statistics and monitoring
     */
    getErrorStats() {
        return {
            total: this.errorStats.totalErrors,
            byType: Object.fromEntries(this.errorStats.errorsByType),
            byCode: Object.fromEntries(this.errorStats.errorsByCode),
            recent: this.errorStats.recentErrors.slice(-10) // Last 10 errors
        };
    }

    getErrorTypeInfo(errorType) {
        return this.errorTypes[errorType] ? { ...this.errorTypes[errorType] } : null;
    }

    getErrorCodeInfo(errorCode) {
        return this.errorCodes[errorCode] ? { ...this.errorCodes[errorCode] } : null;
    }

    resetStats() {
        this.errorStats = {
            totalErrors: 0,
            errorsByType: new Map(),
            errorsByCode: new Map(),
            recentErrors: []
        };
    }

    /**
     * Configuration methods
     */
    updateConfig(newConfig) {
        Object.assign(this.config, newConfig);
    }

    getErrorHandlerInfo() {
        return {
            config: this.config,
            errorTypes: Object.keys(this.errorTypes),
            errorCodes: Object.keys(this.errorCodes),
            recoveryStrategies: Object.keys(this.recoveryStrategies),
            statistics: this.getErrorStats()
        };
    }
}