/**
 * Health Check MCP Tools
 *
 * Tools: ocr_health_check
 *
 * Detects data integrity gaps and optionally triggers fixes.
 * Internal-only - no external API calls needed.
 *
 * CRITICAL: NEVER use console.log() - stdout is reserved for JSON-RPC protocol.
 * Use console.error() for all logging.
 *
 * @module tools/health
 */
import { z } from 'zod';
import { requireDatabase } from '../server/state.js';
import { successResult } from '../server/types.js';
import { validateInput } from '../utils/validation.js';
import { formatResponse, handleError } from './shared.js';
// ═══════════════════════════════════════════════════════════════════════════════
// INPUT SCHEMAS
// ═══════════════════════════════════════════════════════════════════════════════
const HealthCheckInput = z.object({
    fix: z
        .boolean()
        .default(false)
        .describe('If true, trigger processing for fixable gaps (chunks without embeddings). Other gaps are reported but need manual intervention via specific tools.'),
});
// ═══════════════════════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════════════════════
/** Derive a human-readable mode label from the resolved TORCH_DEVICE value. */
function getModeLabel(torchDevice, hasGpu) {
    if (torchDevice === 'cpu')
        return 'cpu-only';
    if (hasGpu)
        return 'gpu';
    return 'unknown';
}
// ═══════════════════════════════════════════════════════════════════════════════
// HANDLER: ocr_health_check
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Handle ocr_health_check - Detect and optionally fix data integrity gaps
 *
 * Checks for:
 * 1. Chunks without embeddings (fixable via embedding generation)
 * 2. Documents without OCR results (non-pending status)
 * 3. Images without VLM descriptions
 * 4. Embeddings without vectors in vec_embeddings
 * 5. Orphaned provenance records
 */
async function handleHealthCheck(params) {
    try {
        const input = validateInput(HealthCheckInput, params);
        const { db, vector } = requireDatabase();
        const conn = db.getConnection();
        const gaps = {};
        const fixes = [];
        const fixFailures = [];
        const SAMPLE_LIMIT = 10;
        // ──────────────────────────────────────────────────────────────
        // Environment Variable Verification
        // ──────────────────────────────────────────────────────────────
        const envStatus = globalThis.__OCR_ENV_STATUS;
        const torchDevice = process.env.TORCH_DEVICE ?? 'auto';
        const embeddingDevice = process.env.EMBEDDING_DEVICE ?? 'auto';
        const isCpuOnly = torchDevice === 'cpu';
        const isUnresolved = torchDevice === 'auto';
        const hasGpu = torchDevice.startsWith('cuda') || torchDevice === 'mps';
        const environment = {
            torch_device: torchDevice,
            chandra_model_path: process.env.CHANDRA_MODEL_PATH ?? null,
            embedding_model_path: process.env.EMBEDDING_MODEL_PATH ?? null,
            embedding_device: embeddingDevice,
            vlm_model: 'chandra-ocr-2',
            vlm_available: hasGpu, // VLM (Chandra OCR 2) requires 10GB+ GPU VRAM
            ocr_engine: 'marker-pdf',
            gpu_enabled: hasGpu,
            mode: getModeLabel(torchDevice, hasGpu),
            is_docker: !!process.env.OCR_PROVENANCE_DATABASES_PATH || !!process.env.OCR_PROVENANCE_ALLOWED_DIRS,
            startup_check: envStatus ?? null,
        };
        const envWarnings = [];
        if (isUnresolved) {
            envWarnings.push('TORCH_DEVICE=auto — device was not resolved at startup. Set TORCH_DEVICE=cpu or TORCH_DEVICE=cuda:0 explicitly, or ensure docker-entrypoint.sh runs GPU detection.');
        }
        if (isCpuOnly) {
            envWarnings.push('Running in CPU-only mode. OCR text extraction (Marker), embeddings (Nomic), and search work on CPU. VLM image description (Chandra OCR 2) requires 10GB+ GPU VRAM and is UNAVAILABLE.');
        }
        // ──────────────────────────────────────────────────────────────
        // Gap 1: Chunks without embeddings
        // ──────────────────────────────────────────────────────────────
        const chunksWithoutEmbeddings = conn
            .prepare(`SELECT c.id FROM chunks c
       LEFT JOIN embeddings e ON e.chunk_id = c.id
       WHERE e.id IS NULL AND c.embedding_status != 'complete'`)
            .all();
        gaps.chunks_without_embeddings = {
            count: chunksWithoutEmbeddings.length,
            sample_ids: chunksWithoutEmbeddings.slice(0, SAMPLE_LIMIT).map((r) => r.id),
            fixable: true,
            fix_tool: 'ocr_process_pending',
            fix_hint: 'Set fix=true to trigger embedding generation',
        };
        // Fix: Generate embeddings for chunks missing them
        if (input.fix && chunksWithoutEmbeddings.length > 0) {
            try {
                const { getEmbeddingService } = await import('../services/embedding/embedder.js');
                const embeddingService = getEmbeddingService();
                // Get pending chunks (those with embedding_status != 'complete')
                const pendingChunks = db.getPendingEmbeddingChunks(100);
                if (pendingChunks.length > 0) {
                    // Group chunks by document for proper provenance
                    const chunksByDoc = new Map();
                    for (const chunk of pendingChunks) {
                        const existing = chunksByDoc.get(chunk.document_id);
                        if (existing) {
                            existing.push(chunk);
                        }
                        else {
                            chunksByDoc.set(chunk.document_id, [chunk]);
                        }
                    }
                    let totalEmbedded = 0;
                    for (const [docId, docChunks] of chunksByDoc.entries()) {
                        const doc = db.getDocument(docId);
                        if (!doc) {
                            console.error(`[HealthCheck] Document ${docId} not found, skipping ${docChunks.length} chunks`);
                            continue;
                        }
                        try {
                            const result = await embeddingService.embedDocumentChunks(db, vector, docChunks, {
                                documentId: doc.id,
                                filePath: doc.file_path,
                                fileName: doc.file_name,
                                fileHash: doc.file_hash,
                                documentProvenanceId: doc.provenance_id,
                            });
                            totalEmbedded += result.totalChunks;
                        }
                        catch (embedError) {
                            // M-16: Track fix failures separately instead of "FAILED:" prefix strings
                            console.error(`[HealthCheck] Failed to embed chunks for ${docId}: ${String(embedError)}`);
                            fixFailures.push({
                                fix: `Embedding generation for document ${docId}`,
                                error: String(embedError),
                            });
                        }
                    }
                    if (totalEmbedded > 0) {
                        fixes.push(`Generated embeddings for ${totalEmbedded} chunks across ${chunksByDoc.size} documents`);
                    }
                }
            }
            catch (serviceError) {
                // M-16: Track fix failures separately instead of "FAILED:" prefix strings
                console.error(`[HealthCheck] Embedding service initialization failed: ${String(serviceError)}`);
                fixFailures.push({
                    fix: 'Embedding service initialization',
                    error: String(serviceError),
                });
            }
        }
        // ──────────────────────────────────────────────────────────────
        // Gap 2: Documents without OCR results (non-pending)
        // ──────────────────────────────────────────────────────────────
        const docsWithoutOCR = conn
            .prepare(`SELECT d.id FROM documents d
       LEFT JOIN ocr_results o ON o.document_id = d.id
       WHERE o.id IS NULL AND d.status NOT IN ('pending', 'processing')`)
            .all();
        gaps.documents_without_ocr = {
            count: docsWithoutOCR.length,
            sample_ids: docsWithoutOCR.slice(0, SAMPLE_LIMIT).map((r) => r.id),
            fixable: false,
            fix_tool: 'ocr_retry_failed',
            fix_hint: 'Use ocr_process_pending to process pending docs, or ocr_retry_failed for failed ones',
        };
        // ──────────────────────────────────────────────────────────────
        // Gap 3: Images without VLM descriptions
        // ──────────────────────────────────────────────────────────────
        const imagesWithoutVLM = conn
            .prepare(`SELECT id FROM images
       WHERE vlm_status IN ('pending', 'failed') OR vlm_status IS NULL`)
            .all();
        gaps.images_without_vlm = {
            count: imagesWithoutVLM.length,
            sample_ids: imagesWithoutVLM.slice(0, SAMPLE_LIMIT).map((r) => r.id),
            fixable: false,
            fix_tool: 'ocr_vlm_process',
            fix_hint: null,
        };
        // ──────────────────────────────────────────────────────────────
        // Gap 4: Embeddings without vectors in vec_embeddings
        // ──────────────────────────────────────────────────────────────
        const embeddingsWithoutVectors = conn
            .prepare(`SELECT e.id FROM embeddings e
       LEFT JOIN vec_embeddings v ON v.embedding_id = e.id
       WHERE v.embedding_id IS NULL`)
            .all();
        gaps.embeddings_without_vectors = {
            count: embeddingsWithoutVectors.length,
            sample_ids: embeddingsWithoutVectors.slice(0, SAMPLE_LIMIT).map((r) => r.id),
            fixable: true,
            fix_tool: 'ocr_health_check',
            fix_hint: 'Set fix=true to delete broken embedding records and clean up associated data',
        };
        // Fix: Delete broken embedding records that have no vector data
        if (input.fix && embeddingsWithoutVectors.length > 0) {
            try {
                let deletedEmbeddings = 0;
                let cleanedProvenance = 0;
                let clearedImageRefs = 0;
                for (const row of embeddingsWithoutVectors) {
                    // 1. NULL out any images.vlm_embedding_id that reference this embedding
                    const imgResult = conn
                        .prepare('UPDATE images SET vlm_embedding_id = NULL WHERE vlm_embedding_id = ?')
                        .run(row.id);
                    clearedImageRefs += imgResult.changes;
                    // 2. Capture the embedding's provenance_id
                    const emb = conn
                        .prepare('SELECT provenance_id FROM embeddings WHERE id = ?')
                        .get(row.id);
                    const provId = emb?.provenance_id ?? null;
                    // 3. Delete the embedding record
                    const delResult = conn.prepare('DELETE FROM embeddings WHERE id = ?').run(row.id);
                    deletedEmbeddings += delResult.changes;
                    // 4. Delete the orphaned provenance record
                    if (provId) {
                        conn.prepare('UPDATE provenance SET parent_id = NULL WHERE parent_id = ?').run(provId);
                        conn.prepare('UPDATE provenance SET source_id = NULL WHERE source_id = ?').run(provId);
                        const provResult = conn.prepare('DELETE FROM provenance WHERE id = ?').run(provId);
                        cleanedProvenance += provResult.changes;
                    }
                }
                fixes.push(`Deleted ${deletedEmbeddings} broken embedding records (no vector data), ` +
                    `cleaned ${cleanedProvenance} orphaned provenance records, ` +
                    `cleared ${clearedImageRefs} stale image references`);
            }
            catch (embFixError) {
                const errMsg = embFixError instanceof Error ? embFixError.message : String(embFixError);
                console.error(`[HealthCheck] Broken embedding cleanup failed: ${errMsg}`);
                fixFailures.push({
                    fix: 'Broken embedding cleanup (embeddings_without_vectors)',
                    error: errMsg,
                });
            }
        }
        // ──────────────────────────────────────────────────────────────
        // Gap 5: Orphaned provenance records
        // Provenance records not referenced by any entity's provenance_id
        // ──────────────────────────────────────────────────────────────
        const orphanedProvenance = conn
            .prepare(`SELECT p.id FROM provenance p
       WHERE p.type = 'DOCUMENT' AND p.id NOT IN (SELECT provenance_id FROM documents WHERE provenance_id IS NOT NULL)
       UNION ALL
       SELECT p.id FROM provenance p
       WHERE p.type = 'OCR_RESULT' AND p.id NOT IN (SELECT provenance_id FROM ocr_results WHERE provenance_id IS NOT NULL)
       UNION ALL
       SELECT p.id FROM provenance p
       WHERE p.type = 'CHUNK' AND p.id NOT IN (SELECT provenance_id FROM chunks WHERE provenance_id IS NOT NULL)
       UNION ALL
       SELECT p.id FROM provenance p
       WHERE p.type = 'EMBEDDING' AND p.id NOT IN (SELECT provenance_id FROM embeddings WHERE provenance_id IS NOT NULL)
       UNION ALL
       SELECT p.id FROM provenance p
       WHERE p.type = 'IMAGE' AND p.id NOT IN (SELECT provenance_id FROM images WHERE provenance_id IS NOT NULL)
       LIMIT 100`)
            .all();
        gaps.orphaned_provenance = {
            count: orphanedProvenance.length,
            sample_ids: orphanedProvenance.slice(0, SAMPLE_LIMIT).map((r) => r.id),
            fixable: true,
            fix_tool: 'ocr_health_check',
            fix_hint: 'Set fix=true to delete orphaned provenance records that reference non-existent entities',
        };
        // Fix: Delete orphaned provenance records
        if (input.fix && orphanedProvenance.length > 0) {
            try {
                const orphanIds = orphanedProvenance.map((r) => r.id);
                // Clear self-references (parent_id/source_id) pointing to orphaned records
                // so deletion doesn't violate provenance self-referential FKs
                for (const orphanId of orphanIds) {
                    conn.prepare('UPDATE provenance SET parent_id = NULL WHERE parent_id = ?').run(orphanId);
                    conn.prepare('UPDATE provenance SET source_id = NULL WHERE source_id = ?').run(orphanId);
                }
                // Delete orphaned provenance records
                let deletedCount = 0;
                for (const orphanId of orphanIds) {
                    const result = conn.prepare('DELETE FROM provenance WHERE id = ?').run(orphanId);
                    deletedCount += result.changes;
                }
                fixes.push(`Deleted ${deletedCount} orphaned provenance records`);
            }
            catch (provCleanupError) {
                const errMsg = provCleanupError instanceof Error ? provCleanupError.message : String(provCleanupError);
                console.error(`[HealthCheck] Orphaned provenance cleanup failed: ${errMsg}`);
                fixFailures.push({
                    fix: 'Orphaned provenance cleanup',
                    error: errMsg,
                });
            }
        }
        // ──────────────────────────────────────────────────────────────
        // Gap 6: Pending documents (ready to process with local OCR)
        // ──────────────────────────────────────────────────────────────
        const pendingDocs = conn
            .prepare(`SELECT id FROM documents WHERE status = 'pending'`)
            .all();
        gaps.pending_documents = {
            count: pendingDocs.length,
            sample_ids: pendingDocs.slice(0, SAMPLE_LIMIT).map((r) => r.id),
            fixable: pendingDocs.length > 0,
            fix_tool: pendingDocs.length > 0 ? 'ocr_process_pending' : null,
            fix_hint: pendingDocs.length > 0 ? 'Run ocr_process_pending to process with local Marker OCR' : null,
        };
        // ──────────────────────────────────────────────────────────────
        // Summary statistics
        // ──────────────────────────────────────────────────────────────
        const totalDocuments = conn.prepare('SELECT COUNT(*) as count FROM documents').get().count;
        const totalChunks = conn.prepare('SELECT COUNT(*) as count FROM chunks').get().count;
        const totalEmbeddings = conn.prepare('SELECT COUNT(*) as count FROM embeddings').get().count;
        const totalImages = conn.prepare('SELECT COUNT(*) as count FROM images').get().count;
        // M-16: If ALL attempted fixes failed, throw an error
        const fixesAttempted = fixes.length + fixFailures.length;
        if (input.fix && fixesAttempted > 0 && fixes.length === 0 && fixFailures.length > 0) {
            const failureDetails = fixFailures.map(f => `  ${f.fix}: ${f.error}`).join('\n');
            throw new Error(`All ${fixFailures.length} fix attempt(s) failed:\n${failureDetails}`);
        }
        const totalGaps = Object.values(gaps).reduce((sum, g) => sum + g.count, 0);
        const healthy = totalGaps === 0 && envWarnings.length === 0;
        // Health checks are diagnostic — do NOT log to activity feed.
        // They run frequently (including via auto-refresh) and would spam the activity log.
        // Build dynamic next_steps based on gaps found
        const nextSteps = [];
        if (gaps.chunks_without_embeddings?.count > 0 || gaps.documents_without_ocr?.count > 0) {
            nextSteps.push({
                tool: 'ocr_process_pending',
                description: 'Process pending documents through the OCR pipeline',
            });
        }
        if (gaps.documents_without_ocr?.count > 0) {
            nextSteps.push({ tool: 'ocr_retry_failed', description: 'Retry failed documents' });
        }
        if (gaps.images_without_vlm?.count > 0) {
            nextSteps.push({
                tool: 'ocr_vlm_process',
                description: 'Generate VLM descriptions for images without them',
            });
        }
        if (envWarnings.length > 0) {
            nextSteps.push({
                tool: 'ocr_guide',
                description: 'View setup instructions for configuring API keys',
            });
        }
        if (nextSteps.length === 0 && healthy) {
            nextSteps.push({ tool: 'ocr_search', description: 'Search across all documents' });
        }
        return formatResponse(successResult({
            healthy,
            environment_status: environment,
            env_warnings: envWarnings.length > 0 ? envWarnings : undefined,
            total_gaps: totalGaps,
            gaps,
            fixes_applied: fixes.length > 0 ? fixes : undefined,
            fix_failures: fixFailures.length > 0 ? fixFailures : undefined,
            fixes_succeeded: fixes.length,
            fixes_failed: fixFailures.length,
            summary: {
                total_documents: totalDocuments,
                total_chunks: totalChunks,
                total_embeddings: totalEmbeddings,
                total_images: totalImages,
            },
            next_steps: nextSteps,
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ═══════════════════════════════════════════════════════════════════════════════
// TOOL DEFINITIONS EXPORT
// ═══════════════════════════════════════════════════════════════════════════════
/**
 * Health check tools collection for MCP server registration
 */
export const healthTools = {
    ocr_health_check: {
        description: '[ESSENTIAL] Diagnose data integrity issues: missing embeddings, orphaned provenance, VLM gaps. Set fix=true to auto-repair.',
        inputSchema: HealthCheckInput.shape,
        handler: handleHealthCheck,
    },
};
//# sourceMappingURL=health.js.map