Source: zpt/api/ResponseFormatter.js

/**
 * Formats API responses with consistent structure and metadata
 */
export default class ResponseFormatter {
    constructor(options = {}) {
        this.config = {
            includeMetadata: options.includeMetadata !== false,
            includeTiming: options.includeTiming !== false,
            includeDebugInfo: options.includeDebugInfo || false,
            compressResponses: options.compressResponses || false,
            standardizeErrors: options.standardizeErrors !== false,
            apiVersion: options.apiVersion || '1.0.0',
            ...options
        };

        this.initializeFormatters();
        this.initializeTemplates();
    }

    /**
     * Initialize response formatters for different content types
     */
    initializeFormatters() {
        this.formatters = {
            success: this.formatSuccessResponse.bind(this),
            error: this.formatErrorResponse.bind(this),
            navigation: this.formatNavigationResponse.bind(this),
            preview: this.formatPreviewResponse.bind(this),
            options: this.formatOptionsResponse.bind(this),
            schema: this.formatSchemaResponse.bind(this),
            health: this.formatHealthResponse.bind(this),
            metrics: this.formatMetricsResponse.bind(this)
        };
    }

    /**
     * Initialize response templates
     */
    initializeTemplates() {
        this.templates = {
            base: {
                success: true,
                data: null,
                metadata: {},
                links: {},
                version: this.config.apiVersion
            },
            error: {
                success: false,
                error: {
                    code: '',
                    message: '',
                    details: null,
                    timestamp: null
                },
                requestId: null,
                version: this.config.apiVersion
            },
            pagination: {
                page: 1,
                limit: 50,
                total: 0,
                hasNext: false,
                hasPrev: false
            }
        };
    }

    /**
     * Main formatting method - routes to appropriate formatter
     * @param {Object} data - Response data to format
     * @param {Object} context - Request context and metadata
     * @returns {Object} Formatted response
     */
    async format(data, context = {}) {
        try {
            // Determine response type
            const responseType = this.determineResponseType(data, context);
            
            // Get appropriate formatter
            const formatter = this.formatters[responseType] || this.formatters.success;
            
            // Format the response
            const formattedResponse = await formatter(data, context);
            
            // Add common metadata
            this.addCommonMetadata(formattedResponse, context);
            
            // Apply post-processing
            return this.postProcessResponse(formattedResponse, context);
            
        } catch (error) {
            // Fallback error formatting
            return this.formatErrorResponse({
                message: `Response formatting failed: ${error.message}`,
                code: 'FORMATTING_ERROR'
            }, context);
        }
    }

    /**
     * Determine response type based on data structure
     */
    determineResponseType(data, context) {
        if (data.success === false || data.error) {
            return 'error';
        }
        
        if (data.navigation && data.content) {
            return 'navigation';
        }
        
        if (data.preview === true) {
            return 'preview';
        }
        
        if (data.options) {
            return 'options';
        }
        
        if (data.schema) {
            return 'schema';
        }
        
        if (data.status && data.checks) {
            return 'health';
        }
        
        if (data.metrics) {
            return 'metrics';
        }
        
        return 'success';
    }

    /**
     * Format success responses
     */
    async formatSuccessResponse(data, context) {
        const response = {
            ...this.templates.base,
            data: this.sanitizeData(data),
            requestId: context.requestId
        };

        // Add timing information
        if (this.config.includeTiming && context.processingTime) {
            response.metadata.timing = {
                processingTime: context.processingTime,
                timestamp: new Date().toISOString()
            };
        }

        // Add pagination if applicable
        if (this.isPaginatedResponse(data)) {
            response.pagination = this.buildPaginationInfo(data, context);
        }

        return response;
    }

    /**
     * Format error responses
     */
    async formatErrorResponse(error, context) {
        const response = {
            ...this.templates.error,
            error: {
                code: error.code || 'UNKNOWN_ERROR',
                message: error.message || 'An unknown error occurred',
                details: this.config.includeDebugInfo ? error.details : null,
                timestamp: new Date().toISOString()
            },
            requestId: context.requestId
        };

        // Add error context if available
        if (error.statusCode) {
            response.error.statusCode = error.statusCode;
        }

        if (error.field) {
            response.error.field = error.field;
        }

        // Add rate limit information for rate limit errors
        if (error.rateLimitInfo) {
            response.error.rateLimit = {
                remaining: error.rateLimitInfo.remaining,
                resetTime: error.rateLimitInfo.resetTime
            };
        }

        return response;
    }

    /**
     * Format navigation responses
     */
    async formatNavigationResponse(data, context) {
        const response = {
            success: true,
            requestId: data.requestId,
            navigation: {
                parameters: this.formatNavigationParameters(data.navigation),
                results: {
                    format: this.detectContentFormat(data.content),
                    size: this.calculateContentSize(data.content),
                    chunks: this.countChunks(data.content)
                }
            },
            content: this.formatNavigationContent(data.content),
            metadata: this.formatNavigationMetadata(data.metadata),
            version: this.config.apiVersion
        };

        // Add diagnostics in debug mode
        if (this.config.includeDebugInfo && data.diagnostics) {
            response.diagnostics = data.diagnostics;
        }

        // Add performance information
        if (data.metadata?.pipeline) {
            response.performance = {
                totalTime: data.metadata.pipeline.totalTime,
                stages: {
                    selection: data.metadata.pipeline.selectionTime,
                    projection: data.metadata.pipeline.projectionTime,
                    transformation: data.metadata.pipeline.transformTime
                }
            };
        }

        return response;
    }

    /**
     * Format preview responses
     */
    async formatPreviewResponse(data, context) {
        const response = {
            success: true,
            requestId: data.requestId,
            preview: {
                navigation: this.formatNavigationParameters(data.navigation),
                summary: data.summary,
                sample: {
                    corpuscles: this.formatSampleCorpuscles(data.corpuscles),
                    count: data.corpuscles?.length || 0
                }
            },
            recommendations: this.generateRecommendations(data),
            version: this.config.apiVersion
        };

        return response;
    }

    /**
     * Format options responses
     */
    async formatOptionsResponse(data, context) {
        const response = {
            success: true,
            requestId: data.requestId,
            options: {
                navigation: {
                    zoom: {
                        levels: data.options.zoomLevels,
                        descriptions: this.getZoomDescriptions()
                    },
                    tilt: {
                        representations: data.options.tiltRepresentations,
                        descriptions: this.getTiltDescriptions()
                    },
                    pan: {
                        filters: ['topic', 'entity', 'temporal', 'geographic'],
                        descriptions: this.getPanDescriptions()
                    }
                },
                output: {
                    formats: data.options.outputFormats,
                    encodings: data.options.encodingStrategies,
                    tokenLimits: data.options.maxTokenLimits
                },
                technical: {
                    tokenizers: data.options.supportedTokenizers,
                    models: Object.keys(data.options.maxTokenLimits)
                }
            },
            examples: this.generateUsageExamples(),
            version: this.config.apiVersion
        };

        return response;
    }

    /**
     * Format schema responses
     */
    async formatSchemaResponse(data, context) {
        const response = {
            success: true,
            requestId: data.requestId,
            schema: {
                parameters: data.schema,
                validation: {
                    required: this.extractRequiredFields(data.schema),
                    optional: this.extractOptionalFields(data.schema)
                },
                examples: data.examples,
                documentation: data.documentation
            },
            version: this.config.apiVersion
        };

        return response;
    }

    /**
     * Format health check responses
     */
    async formatHealthResponse(data, context) {
        const response = {
            success: true,
            requestId: data.requestId,
            health: {
                status: data.status,
                checks: this.formatHealthChecks(data.checks),
                system: {
                    activeRequests: data.activeRequests,
                    uptime: data.uptime,
                    timestamp: new Date().toISOString()
                }
            },
            version: this.config.apiVersion
        };

        return response;
    }

    /**
     * Format metrics responses
     */
    async formatMetricsResponse(data, context) {
        const response = {
            success: true,
            requestId: data.requestId,
            metrics: {
                api: this.formatApiMetrics(data.metrics.requests),
                components: this.formatComponentMetrics(data.metrics.components),
                system: this.formatSystemMetrics(data.metrics.system),
                timestamp: new Date().toISOString()
            },
            version: this.config.apiVersion
        };

        return response;
    }

    /**
     * Helper methods for data formatting
     */
    formatNavigationParameters(navigation) {
        return {
            zoom: navigation.zoom?.level || navigation.zoom,
            pan: this.formatPanParameters(navigation.pan),
            tilt: navigation.tilt?.representation || navigation.tilt,
            transform: this.formatTransformParameters(navigation.transform)
        };
    }

    formatPanParameters(pan) {
        if (!pan || Object.keys(pan).length === 0) {
            return { applied: false };
        }

        const formatted = { applied: true, filters: {} };

        if (pan.topic) {
            formatted.filters.topic = {
                value: pan.topic.value || pan.topic,
                pattern: pan.topic.pattern || 'exact'
            };
        }

        if (pan.entity) {
            formatted.filters.entity = {
                count: pan.entity.count || (Array.isArray(pan.entity) ? pan.entity.length : 1),
                type: pan.entity.type || 'multiple'
            };
        }

        if (pan.temporal) {
            formatted.filters.temporal = {
                hasRange: !!(pan.temporal.start && pan.temporal.end),
                duration: pan.temporal.durationDays
            };
        }

        if (pan.geographic) {
            formatted.filters.geographic = {
                type: pan.geographic.bbox ? 'bbox' : pan.geographic.center ? 'center' : 'unknown',
                area: this.calculateGeographicArea(pan.geographic)
            };
        }

        return formatted;
    }

    formatTransformParameters(transform) {
        if (!transform) return { default: true };

        return {
            maxTokens: transform.maxTokens,
            format: transform.format,
            tokenizer: transform.tokenizer,
            strategy: transform.chunkStrategy,
            metadata: transform.includeMetadata
        };
    }

    formatNavigationContent(content) {
        if (!content) return null;

        if (Array.isArray(content)) {
            // Chunked content
            return {
                type: 'chunked',
                chunks: content.map((chunk, index) => ({
                    id: chunk.chunkId || `chunk_${index}`,
                    size: this.calculateChunkSize(chunk),
                    preview: this.generateContentPreview(chunk)
                })),
                totalChunks: content.length
            };
        } else {
            // Single content
            return {
                type: 'single',
                size: this.calculateContentSize(content),
                format: this.detectContentFormat(content),
                preview: this.generateContentPreview(content)
            };
        }
    }

    formatNavigationMetadata(metadata) {
        if (!metadata) return null;

        const formatted = {
            processing: {
                totalTime: metadata.pipeline?.totalTime,
                stages: metadata.pipeline ? Object.keys(metadata.pipeline).length - 1 : 0
            }
        };

        if (metadata.selection) {
            formatted.selection = {
                corpuscleCount: metadata.selection.resultCount,
                fromCache: metadata.selection.fromCache,
                complexity: metadata.selection.complexity
            };
        }

        if (metadata.projection) {
            formatted.projection = {
                type: metadata.projection.representation,
                processingTime: metadata.projection.processingTime
            };
        }

        if (metadata.transformation) {
            formatted.transformation = {
                format: metadata.transformation.output?.format,
                chunked: metadata.transformation.output?.chunked,
                hasMetadata: metadata.transformation.output?.hasMetadata
            };
        }

        return formatted;
    }

    formatSampleCorpuscles(corpuscles) {
        if (!corpuscles || corpuscles.length === 0) return [];

        return corpuscles.map(corpuscle => ({
            uri: corpuscle.uri,
            type: corpuscle.type,
            score: corpuscle.score,
            preview: this.generateCorpusclePreview(corpuscle)
        }));
    }

    formatHealthChecks(checks) {
        const formatted = {};

        for (const [component, check] of Object.entries(checks)) {
            if (check === null) continue;

            formatted[component] = {
                status: check.status,
                ...(check.metrics && { metrics: check.metrics }),
                ...(check.error && { error: check.error }),
                ...(check.issues && { issues: check.issues })
            };
        }

        return formatted;
    }

    formatApiMetrics(requestMetrics) {
        return {
            requests: {
                total: requestMetrics.totalRequests,
                successful: requestMetrics.successfulRequests,
                failed: requestMetrics.failedRequests,
                successRate: requestMetrics.totalRequests > 0 ? 
                    (requestMetrics.successfulRequests / requestMetrics.totalRequests * 100).toFixed(2) + '%' : '0%'
            },
            performance: {
                avgResponseTime: Math.round(requestMetrics.avgResponseTime),
                unit: 'ms'
            },
            endpoints: this.formatEndpointMetrics(requestMetrics.requestsByEndpoint)
        };
    }

    formatEndpointMetrics(endpointMap) {
        const formatted = {};

        for (const [endpoint, stats] of endpointMap.entries()) {
            formatted[endpoint] = {
                total: stats.total,
                successful: stats.successful,
                successRate: stats.total > 0 ? 
                    (stats.successful / stats.total * 100).toFixed(2) + '%' : '0%'
            };
        }

        return formatted;
    }

    formatComponentMetrics(componentMetrics) {
        const formatted = {};

        for (const [component, metrics] of Object.entries(componentMetrics || {})) {
            if (metrics) {
                formatted[component] = {
                    ...metrics,
                    // Add computed fields if available
                    ...(metrics.cacheHitRate !== undefined && {
                        cachePerformance: (metrics.cacheHitRate * 100).toFixed(1) + '%'
                    })
                };
            }
        }

        return formatted;
    }

    formatSystemMetrics(systemMetrics) {
        return {
            activeRequests: systemMetrics.activeRequests,
            memory: {
                rss: Math.round(systemMetrics.memoryUsage.rss / 1024 / 1024) + ' MB',
                heapUsed: Math.round(systemMetrics.memoryUsage.heapUsed / 1024 / 1024) + ' MB',
                heapTotal: Math.round(systemMetrics.memoryUsage.heapTotal / 1024 / 1024) + ' MB'
            },
            uptime: {
                seconds: Math.round(systemMetrics.uptime),
                formatted: this.formatUptime(systemMetrics.uptime)
            }
        };
    }

    /**
     * Utility methods
     */
    sanitizeData(data) {
        // Remove sensitive information and circular references
        return this.removeCircularReferences(data);
    }

    removeCircularReferences(obj, seen = new WeakSet()) {
        if (obj === null || typeof obj !== 'object') return obj;
        if (seen.has(obj)) return '[Circular Reference]';

        seen.add(obj);

        if (Array.isArray(obj)) {
            return obj.map(item => this.removeCircularReferences(item, seen));
        }

        const cleaned = {};
        for (const [key, value] of Object.entries(obj)) {
            // Skip sensitive fields
            if (this.isSensitiveField(key)) continue;
            
            cleaned[key] = this.removeCircularReferences(value, seen);
        }

        return cleaned;
    }

    isSensitiveField(fieldName) {
        const sensitiveFields = ['password', 'secret', 'token', 'key', 'auth'];
        return sensitiveFields.some(field => 
            fieldName.toLowerCase().includes(field)
        );
    }

    detectContentFormat(content) {
        if (!content) return 'unknown';
        
        if (typeof content === 'string') {
            if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
                return 'json';
            }
            if (content.includes('# ') || content.includes('## ')) {
                return 'markdown';
            }
            return 'text';
        }
        
        if (typeof content === 'object') {
            return 'structured';
        }
        
        return 'unknown';
    }

    calculateContentSize(content) {
        if (!content) return 0;
        
        if (typeof content === 'string') {
            return content.length;
        }
        
        return JSON.stringify(content).length;
    }

    calculateChunkSize(chunk) {
        if (!chunk) return 0;
        
        const content = chunk.encoded?.content || chunk.formatted?.content || chunk.content || chunk;
        return this.calculateContentSize(content);
    }

    countChunks(content) {
        if (Array.isArray(content)) {
            return content.length;
        }
        return 1;
    }

    generateContentPreview(content, maxLength = 200) {
        let text = '';
        
        if (typeof content === 'string') {
            text = content;
        } else if (content?.encoded?.content) {
            text = content.encoded.content;
        } else if (content?.formatted?.content) {
            text = content.formatted.content;
        } else if (content?.content) {
            text = content.content;
        } else {
            text = JSON.stringify(content);
        }

        return text.length > maxLength ? 
            text.substring(0, maxLength) + '...' : text;
    }

    generateCorpusclePreview(corpuscle) {
        const content = corpuscle.content || {};
        const preview = content.label || content.prefLabel || content.text || content.content;
        
        if (typeof preview === 'string') {
            return preview.length > 100 ? preview.substring(0, 100) + '...' : preview;
        }
        
        return corpuscle.uri || 'Unknown';
    }

    calculateGeographicArea(geographic) {
        if (geographic.bbox) {
            const { minLon, minLat, maxLon, maxLat } = geographic.bbox;
            return (maxLon - minLon) * (maxLat - minLat);
        }
        
        if (geographic.radius) {
            return Math.PI * geographic.radius * geographic.radius;
        }
        
        return 0;
    }

    formatUptime(seconds) {
        const days = Math.floor(seconds / 86400);
        const hours = Math.floor((seconds % 86400) / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        
        if (days > 0) {
            return `${days}d ${hours}h ${minutes}m`;
        } else if (hours > 0) {
            return `${hours}h ${minutes}m`;
        } else {
            return `${minutes}m`;
        }
    }

    isPaginatedResponse(data) {
        return data.page !== undefined || data.limit !== undefined || data.total !== undefined;
    }

    buildPaginationInfo(data, context) {
        return {
            page: data.page || 1,
            limit: data.limit || 50,
            total: data.total || 0,
            hasNext: data.hasNext || false,
            hasPrev: data.hasPrev || false
        };
    }

    addCommonMetadata(response, context) {
        if (!this.config.includeMetadata) return;

        response.metadata = response.metadata || {};
        
        if (context.endpoint) {
            response.metadata.endpoint = context.endpoint;
        }
        
        if (this.config.includeTiming && context.processingTime) {
            response.metadata.timing = {
                processingTime: context.processingTime,
                timestamp: new Date().toISOString()
            };
        }
    }

    postProcessResponse(response, context) {
        // Add HATEOAS links if applicable
        if (context.addLinks) {
            response.links = this.generateHateoasLinks(response, context);
        }

        // Compress response if enabled
        if (this.config.compressResponses) {
            response = this.compressResponse(response);
        }

        return response;
    }

    generateHateoasLinks(response, context) {
        const links = {};
        
        if (response.navigation) {
            links.self = `/api/navigate`;
            links.preview = `/api/navigate/preview`;
            links.options = `/api/navigate/options`;
            links.schema = `/api/navigate/schema`;
        }
        
        return links;
    }

    compressResponse(response) {
        // Simple compression - remove empty fields
        return this.removeEmptyFields(response);
    }

    removeEmptyFields(obj) {
        if (Array.isArray(obj)) {
            return obj.map(item => this.removeEmptyFields(item)).filter(item => item !== null);
        }
        
        if (obj && typeof obj === 'object') {
            const cleaned = {};
            for (const [key, value] of Object.entries(obj)) {
                const cleanedValue = this.removeEmptyFields(value);
                if (cleanedValue !== null && cleanedValue !== undefined && cleanedValue !== '') {
                    cleaned[key] = cleanedValue;
                }
            }
            return Object.keys(cleaned).length > 0 ? cleaned : null;
        }
        
        return obj;
    }

    /**
     * Configuration and documentation helpers
     */
    getZoomDescriptions() {
        return {
            entity: 'Individual entities and concepts',
            unit: 'Semantic units and meaningful segments',
            text: 'Full text elements and documents',
            community: 'Thematic communities and clusters',
            corpus: 'Entire corpus overview and metadata'
        };
    }

    getTiltDescriptions() {
        return {
            embedding: 'Vector embeddings and similarity analysis',
            keywords: 'Keyword extraction and term analysis',
            graph: 'Graph structure and relationship analysis',
            temporal: 'Temporal sequences and chronological analysis'
        };
    }

    getPanDescriptions() {
        return {
            topic: 'Filter by subject area or theme',
            entity: 'Filter by specific entities or concepts',
            temporal: 'Filter by time period or date range',
            geographic: 'Filter by location or geographic area'
        };
    }

    generateUsageExamples() {
        return {
            basic: {
                description: 'Basic navigation request',
                request: {
                    zoom: 'unit',
                    tilt: 'keywords'
                }
            },
            filtered: {
                description: 'Navigation with topic filter',
                request: {
                    zoom: 'entity',
                    pan: { topic: 'machine-learning' },
                    tilt: 'embedding'
                }
            },
            advanced: {
                description: 'Advanced navigation with multiple filters',
                request: {
                    zoom: 'text',
                    pan: {
                        topic: 'artificial-intelligence',
                        temporal: {
                            start: '2024-01-01',
                            end: '2024-12-31'
                        }
                    },
                    tilt: 'graph',
                    transform: {
                        maxTokens: 8000,
                        format: 'markdown'
                    }
                }
            }
        };
    }

    extractRequiredFields(schema) {
        const required = [];
        
        for (const [field, config] of Object.entries(schema)) {
            if (config.required) {
                required.push(field);
            }
        }
        
        return required;
    }

    extractOptionalFields(schema) {
        const optional = [];
        
        for (const [field, config] of Object.entries(schema)) {
            if (!config.required) {
                optional.push(field);
            }
        }
        
        return optional;
    }

    generateRecommendations(previewData) {
        const recommendations = [];
        
        if (previewData.summary?.estimatedTokens > 8000) {
            recommendations.push({
                type: 'token_optimization',
                message: 'Consider using chunking for large content',
                suggestion: 'Add "chunkStrategy": "semantic" to transform parameters'
            });
        }
        
        if (previewData.summary?.corpuscleCount < 5) {
            recommendations.push({
                type: 'scope_expansion',
                message: 'Few results found, consider broadening filters',
                suggestion: 'Try using a higher zoom level or removing some pan filters'
            });
        }
        
        if (previewData.summary?.complexity > 8) {
            recommendations.push({
                type: 'complexity_reduction',
                message: 'High complexity detected',
                suggestion: 'Consider simplifying pan filters for better performance'
            });
        }
        
        return recommendations;
    }

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

    getFormatterInfo() {
        return {
            config: this.config,
            formatters: Object.keys(this.formatters),
            templates: Object.keys(this.templates)
        };
    }
}