/**
 * Local VLM (Vision Language Model) MCP Tools
 *
 * Tools for Chandra local GPU image analysis of legal and medical documents.
 * Uses ChandraClient for analysis and VLMPipeline for batch processing.
 *
 * 100% local GPU processing via Chandra VLM.
 * NO cloud API calls, NO fallbacks.
 *
 * CRITICAL: NEVER use console.log() - stdout is reserved for JSON-RPC protocol.
 * Use console.error() for all logging.
 *
 * @module tools/vlm-local
 */
import { z } from 'zod';
import * as fs from 'fs';
import { state, requireDatabase, withDatabaseOperation } from '../server/state.js';
import { RegistryService } from '../services/storage/registry/index.js';
import { successResult } from '../server/types.js';
import { MCPError } from '../server/errors.js';
import { logAudit } from '../services/audit.js';
import { formatResponse, handleError } from './shared.js';
import { validateInput } from '../utils/validation.js';
import { getChandraClient } from '../services/vlm/chandra.js';
import { VLMPipeline } from '../services/vlm/pipeline.js';
// ===============================================================================
// GPU GATE: VLM requires a GPU with 10GB+ VRAM (Chandra OCR 2 ~5B parameter model)
// On CPU-only systems, VLM tools return a clear error. All other features
// (OCR, embeddings, search, image extraction) continue working normally.
// ===============================================================================
/**
 * VLM is only available when TORCH_DEVICE is explicitly a GPU device.
 * 'auto' means the entrypoint hasn't resolved it yet (misconfiguration).
 * Matches the logic in health.ts: vlm_available = device starts with 'cuda' or is 'mps'.
 */
export function isVLMAvailable() {
    const device = process.env.TORCH_DEVICE ?? 'auto';
    return device.startsWith('cuda') || device === 'mps';
}
function vlmUnavailableError() {
    const device = process.env.TORCH_DEVICE ?? 'auto';
    console.error(`[vlm] VLM unavailable: TORCH_DEVICE=${device}. Chandra OCR 2 requires a GPU with 10GB+ VRAM.`);
    return formatResponse(successResult({
        error: 'VLM unavailable on CPU',
        message: `VLM (Chandra OCR 2) requires a GPU with 10GB+ VRAM. Current device: ${device}. ` +
            'OCR, embeddings, search, and image extraction continue to work normally. ' +
            'To enable VLM, run the container on a system with an NVIDIA GPU (10GB+ VRAM).',
        torch_device: device,
        vlm_available: false,
        next_steps: [
            { tool: 'ocr_process_pending', description: '[PROCESSING] Process documents (OCR + embeddings, VLM skipped on CPU)' },
            { tool: 'ocr_search', description: '[SEARCH] Search documents by text' },
            { tool: 'ocr_image_list', description: '[STATUS] List extracted images (descriptions unavailable on CPU)' },
        ],
    }));
}
// ===============================================================================
// VALIDATION SCHEMAS
// ===============================================================================
const VLMDescribeInput = z.object({
    image_id: z.string().min(1),
    context_text: z.string().optional(),
});
const VLMProcessInput = z.object({
    document_id: z.string().optional(),
    batch_size: z.number().int().min(1).max(20).default(5),
    skip_processed: z.boolean().default(true),
});
const VLMStatusInput = z.object({});
// ===============================================================================
// VLM TOOL HANDLERS
// ===============================================================================
/**
 * Handle ocr_vlm_describe - Generate detailed description of a single image using Chandra VLM
 *
 * Looks up the image in the database by image_id, runs local VLM analysis,
 * stores the result in the DB (updates images table vlm_description, vlm_model),
 * creates provenance records, and generates an embedding for searchability.
 */
export async function handleVLMDescribe(params) {
    if (!isVLMAvailable())
        return vlmUnavailableError();
    try {
        const input = validateInput(VLMDescribeInput, params);
        const imageId = input.image_id;
        const contextText = input.context_text;
        return await withDatabaseOperation(async () => {
            const { db, vector } = requireDatabase();
            const conn = db.getConnection();
            // Look up image by ID
            const img = conn
                .prepare('SELECT id, document_id, page_number, image_index, extracted_path, provenance_id, vlm_description, vlm_model FROM images WHERE id = ?')
                .get(imageId);
            if (!img) {
                throw new MCPError('DOCUMENT_NOT_FOUND', `Image not found: ${imageId}`, {
                    image_id: imageId,
                });
            }
            if (!img.extracted_path) {
                throw new MCPError('PATH_NOT_FOUND', `Image has no extracted_path: ${imageId}`, {
                    image_id: imageId,
                });
            }
            if (!fs.existsSync(img.extracted_path)) {
                throw new MCPError('PATH_NOT_FOUND', `Image file not found on disk: ${img.extracted_path}`, {
                    image_id: imageId,
                    extracted_path: img.extracted_path,
                });
            }
            // Enrich context with table metadata
            let enrichedContext = contextText;
            try {
                if (img.page_number != null) {
                    const tableRows = conn
                        .prepare("SELECT processing_params FROM provenance WHERE root_document_id IN (SELECT provenance_id FROM documents WHERE id = ?) AND processing_params LIKE '%table_columns%' AND json_extract(processing_params, '$.character_start') IS NOT NULL")
                        .all(img.document_id);
                    const tableContextParts = [];
                    for (const row of tableRows) {
                        try {
                            const rowParams = JSON.parse(row.processing_params);
                            const cols = rowParams.table_columns;
                            const summary = rowParams.table_summary;
                            if (cols && cols.length > 0) {
                                const part = summary ?? `Table with columns: ${cols.join(', ')}`;
                                tableContextParts.push(part);
                            }
                        }
                        catch (parseErr) {
                            console.error(`[vlm-local] Failed to parse table processing_params: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
                        }
                    }
                    if (tableContextParts.length > 0) {
                        const tableContext = `This page contains structured table data. ${tableContextParts.join('. ')}`;
                        enrichedContext = enrichedContext
                            ? `${enrichedContext}\n\n${tableContext}`
                            : tableContext;
                    }
                }
            }
            catch (ctxError) {
                console.error(`[vlm-local] Failed to enrich VLM context for image ${imageId}: ${ctxError instanceof Error ? ctxError.message : String(ctxError)}`);
            }
            // Run Chandra VLM analysis
            const chandra = getChandraClient();
            const result = await chandra.describeImage(img.extracted_path, {
                contextText: enrichedContext,
            });
            // Update images table with VLM result
            conn
                .prepare("UPDATE images SET vlm_description = ?, vlm_model = ?, vlm_status = 'complete' WHERE id = ?")
                .run(result.description, result.model, imageId);
            // Create provenance and embedding for the VLM description
            let embeddingId = null;
            let embeddingGenerated = false;
            if (img.provenance_id && result.description) {
                try {
                    const { getEmbeddingClient, MODEL_NAME: EMBEDDING_MODEL } = await import('../services/embedding/nomic.js');
                    const { v4: uuidv4 } = await import('uuid');
                    const { computeHash } = await import('../utils/hash.js');
                    const { ProvenanceType } = await import('../models/provenance.js');
                    const embeddingClient = getEmbeddingClient();
                    const vectors = await embeddingClient.embedChunks([result.description], 1);
                    if (vectors.length > 0) {
                        const embId = uuidv4();
                        const now = new Date().toISOString();
                        const descriptionHash = computeHash(result.description);
                        // Get IMAGE provenance to build chain
                        const imageProv = db.getProvenance(img.provenance_id);
                        if (imageProv) {
                            // Create VLM_DESCRIPTION provenance (depth 3)
                            const vlmDescProvId = uuidv4();
                            const imageParentIds = JSON.parse(imageProv.parent_ids);
                            const vlmParentIds = [...imageParentIds, img.provenance_id];
                            db.insertProvenance({
                                id: vlmDescProvId,
                                type: ProvenanceType.VLM_DESCRIPTION,
                                created_at: now,
                                processed_at: now,
                                source_file_created_at: null,
                                source_file_modified_at: null,
                                source_type: 'VLM',
                                source_path: img.extracted_path,
                                source_id: img.provenance_id,
                                root_document_id: imageProv.root_document_id,
                                location: {
                                    page_number: img.page_number,
                                    chunk_index: img.image_index,
                                },
                                content_hash: descriptionHash,
                                input_hash: imageProv.content_hash,
                                file_hash: imageProv.file_hash,
                                processor: 'chandra-vlm:describe',
                                processor_version: '0.2.0',
                                processing_params: { type: 'vlm_describe', model: 'chandra' },
                                processing_duration_ms: result.processingTimeMs ?? null,
                                processing_quality_score: null,
                                parent_id: img.provenance_id,
                                parent_ids: JSON.stringify(vlmParentIds),
                                chain_depth: 3,
                                chain_path: JSON.stringify(['DOCUMENT', 'OCR_RESULT', 'IMAGE', 'VLM_DESCRIPTION']),
                            });
                            // Create EMBEDDING provenance (depth 4)
                            const embProvId = uuidv4();
                            const embParentIds = [...vlmParentIds, vlmDescProvId];
                            db.insertProvenance({
                                id: embProvId,
                                type: ProvenanceType.EMBEDDING,
                                created_at: now,
                                processed_at: now,
                                source_file_created_at: null,
                                source_file_modified_at: null,
                                source_type: 'EMBEDDING',
                                source_path: null,
                                source_id: vlmDescProvId,
                                root_document_id: imageProv.root_document_id,
                                location: {
                                    page_number: img.page_number,
                                    chunk_index: img.image_index,
                                },
                                content_hash: descriptionHash,
                                input_hash: descriptionHash,
                                file_hash: imageProv.file_hash,
                                processor: EMBEDDING_MODEL,
                                processor_version: '1.5.0',
                                processing_params: { task_type: 'search_document', dimensions: 768 },
                                processing_duration_ms: null,
                                processing_quality_score: null,
                                parent_id: vlmDescProvId,
                                parent_ids: JSON.stringify(embParentIds),
                                chain_depth: 4,
                                chain_path: JSON.stringify([
                                    'DOCUMENT',
                                    'OCR_RESULT',
                                    'IMAGE',
                                    'VLM_DESCRIPTION',
                                    'EMBEDDING',
                                ]),
                            });
                            // Insert embedding record
                            db.insertEmbedding({
                                id: embId,
                                chunk_id: null,
                                image_id: img.id,
                                extraction_id: null,
                                document_id: img.document_id,
                                original_text: result.description,
                                original_text_length: result.description.length,
                                source_file_path: img.extracted_path ?? 'unknown',
                                source_file_name: img.extracted_path?.split('/').pop() ?? 'vlm_description',
                                source_file_hash: 'vlm_generated',
                                page_number: img.page_number,
                                page_range: null,
                                character_start: 0,
                                character_end: result.description.length,
                                chunk_index: img.image_index,
                                total_chunks: 1,
                                model_name: EMBEDDING_MODEL,
                                model_version: '1.5.0',
                                task_type: 'search_document',
                                inference_mode: 'local',
                                gpu_device: process.env.TORCH_DEVICE ?? 'cuda:0',
                                provenance_id: embProvId,
                                content_hash: descriptionHash,
                                generation_duration_ms: null,
                            });
                            // Store vector
                            vector.storeVector(embId, vectors[0]);
                            // Update image.vlm_embedding_id
                            conn
                                .prepare('UPDATE images SET vlm_embedding_id = ? WHERE id = ?')
                                .run(embId, img.id);
                            embeddingId = embId;
                            embeddingGenerated = true;
                            console.error(`[INFO] VLM describe embedding generated for image ${img.id}: ${embId}`);
                        }
                    }
                }
                catch (embError) {
                    const errMsg = embError instanceof Error ? embError.message : String(embError);
                    console.error(`[ERROR] VLM describe embedding generation failed: ${errMsg}`);
                    throw new Error(`VLM description stored but embedding failed: ${errMsg}. Description is not searchable.`);
                }
            }
            logAudit({
                action: 'vlm_describe',
                entityType: 'image',
                entityId: imageId,
                details: {
                    model: result.model,
                    tokens_used: result.tokensUsed,
                    embedding_generated: embeddingGenerated,
                },
            });
            return formatResponse(successResult({
                image_id: imageId,
                description: result.description,
                analysis: result.analysis,
                model: result.model,
                processing_time_ms: result.processingTimeMs,
                tokens_used: result.tokensUsed,
                confidence: result.analysis.confidence,
                embedding_id: embeddingId,
                embedding_generated: embeddingGenerated,
                next_steps: [
                    {
                        tool: 'ocr_image_get',
                        description: 'View image details including the new description',
                    },
                    {
                        tool: 'ocr_image_search',
                        description: 'Search for similar images (mode=semantic for meaning-based)',
                    },
                ],
            }));
        }); // end withDatabaseOperation
    }
    catch (error) {
        return handleError(error);
    }
}
/**
 * Handle ocr_vlm_process - Process VLM on a single document or all pending images
 *
 * If document_id is provided, processes all images in that document.
 * If document_id is omitted, processes all pending images across all documents.
 */
export async function handleVLMProcess(params) {
    if (!isVLMAvailable())
        return vlmUnavailableError();
    try {
        const input = validateInput(VLMProcessInput, params);
        const documentId = input.document_id;
        const batchSize = input.batch_size ?? 5;
        return await withDatabaseOperation(async () => {
            const { db, vector } = requireDatabase();
            const conn = db.getConnection();
            if (documentId) {
                // Single document mode
                const doc = db.getDocument(documentId);
                if (!doc) {
                    throw new MCPError('DOCUMENT_NOT_FOUND', `Document not found: ${documentId}`, {
                        document_id: documentId,
                    });
                }
                const pipeline = new VLMPipeline(conn, {
                    config: {
                        batchSize,
                        concurrency: 5,
                        minConfidence: 0.5,
                        skipEmbeddings: false,
                        skipProvenance: false,
                    },
                    dbService: db,
                    vectorService: vector,
                });
                const result = await pipeline.processDocument(documentId);
                logAudit({
                    action: 'vlm_process',
                    entityType: 'document',
                    entityId: documentId,
                    details: {
                        total: result.total,
                        successful: result.successful,
                        failed: result.failed,
                        total_tokens: result.totalTokens,
                    },
                });
                try {
                    const dbName = state.currentDatabaseName;
                    if (dbName) {
                        RegistryService.getInstance().recordAccess(dbName, 'vlm_process', `VLM processed ${result.successful} images`);
                    }
                }
                catch { /* activity logging must never break the main operation */ }
                const responseData = {
                    mode: 'document',
                    document_id: documentId,
                    total: result.total,
                    successful: result.successful,
                    failed: result.failed,
                    total_tokens: result.totalTokens,
                    processing_time_ms: result.totalTimeMs,
                    results: result.results.map((r) => ({
                        image_id: r.imageId,
                        success: r.success,
                        confidence: r.confidence,
                        tokens_used: r.tokensUsed,
                        error: r.error,
                    })),
                    next_steps: [
                        { tool: 'ocr_image_list', description: 'Browse all images for this document' },
                        { tool: 'ocr_vlm_status', description: 'Check VLM processing status' },
                    ],
                };
                return formatResponse(successResult(responseData));
            }
            else {
                // All pending mode
                const pipeline = new VLMPipeline(conn, {
                    config: {
                        batchSize,
                        concurrency: 5,
                        minConfidence: 0.5,
                        skipEmbeddings: false,
                        skipProvenance: false,
                    },
                    dbService: db,
                    vectorService: vector,
                });
                const result = await pipeline.processPending(50);
                logAudit({
                    action: 'vlm_process_pending',
                    entityType: 'image',
                    details: {
                        processed: result.total,
                        successful: result.successful,
                        failed: result.failed,
                        total_tokens: result.totalTokens,
                    },
                });
                try {
                    const dbName = state.currentDatabaseName;
                    if (dbName) {
                        RegistryService.getInstance().recordAccess(dbName, 'vlm_process', `VLM processed ${result.successful} images`);
                    }
                }
                catch { /* activity logging must never break the main operation */ }
                const responseData = {
                    mode: 'pending',
                    processed: result.total,
                    successful: result.successful,
                    failed: result.failed,
                    total_tokens: result.totalTokens,
                    processing_time_ms: result.totalTimeMs,
                    next_steps: [
                        { tool: 'ocr_document_list', description: 'Browse all documents in the database' },
                        { tool: 'ocr_vlm_status', description: 'Check VLM processing status' },
                    ],
                };
                return formatResponse(successResult(responseData));
            }
        }); // end withDatabaseOperation
    }
    catch (error) {
        return handleError(error);
    }
}
/**
 * Handle ocr_vlm_status - Get VLM processing status and statistics
 *
 * Returns stats about processed/pending/failed images and model info.
 * Works on the currently selected database.
 */
export async function handleVLMStatus(params) {
    try {
        validateInput(VLMStatusInput, params);
        const device = process.env.TORCH_DEVICE ?? 'auto';
        if (!isVLMAvailable()) {
            // Still report image stats on CPU — images exist but won't be described
            let imageStats = null;
            try {
                const { db } = requireDatabase();
                const conn = db.getConnection();
                const totalImages = conn.prepare('SELECT COUNT(*) as count FROM images').get().count;
                const completedImages = conn.prepare("SELECT COUNT(*) as count FROM images WHERE vlm_status = 'complete'").get().count;
                const pendingImages = conn.prepare("SELECT COUNT(*) as count FROM images WHERE vlm_status = 'pending' OR vlm_status IS NULL").get().count;
                const failedImages = conn.prepare("SELECT COUNT(*) as count FROM images WHERE vlm_status = 'failed'").get().count;
                imageStats = { total: totalImages, completed: completedImages, pending: pendingImages, failed: failedImages, processing: 0 };
            }
            catch { /* no database selected */ }
            return formatResponse(successResult({
                vlm_available: false,
                torch_device: device,
                message: `VLM (Chandra OCR 2) requires a GPU with 10GB+ VRAM. Current device: ${device}. VLM features disabled.`,
                image_stats: imageStats,
                next_steps: [
                    { tool: 'ocr_image_list', description: '[STATUS] List extracted images' },
                    { tool: 'ocr_process_pending', description: '[PROCESSING] Process documents (OCR + embeddings)' },
                ],
            }));
        }
        const chandra = getChandraClient();
        const status = chandra.getStatus();
        // Get image processing stats from database if available
        let imageStats = null;
        try {
            const { db } = requireDatabase();
            const conn = db.getConnection();
            const totalImages = conn.prepare('SELECT COUNT(*) as count FROM images').get().count;
            const completedImages = conn.prepare("SELECT COUNT(*) as count FROM images WHERE vlm_status = 'complete'").get().count;
            const pendingImages = conn
                .prepare("SELECT COUNT(*) as count FROM images WHERE vlm_status = 'pending' OR vlm_status IS NULL")
                .get().count;
            const failedImages = conn.prepare("SELECT COUNT(*) as count FROM images WHERE vlm_status = 'failed'").get().count;
            const processingImages = conn.prepare("SELECT COUNT(*) as count FROM images WHERE vlm_status = 'processing'").get().count;
            imageStats = {
                total: totalImages,
                completed: completedImages,
                pending: pendingImages,
                failed: failedImages,
                processing: processingImages,
            };
        }
        catch {
            // No database selected - stats unavailable
        }
        return formatResponse(successResult({
            model: status.model,
            method: status.method,
            batch_size: status.batchSize,
            torch_device: process.env.TORCH_DEVICE ?? 'auto',
            chandra_model_path: process.env.CHANDRA_MODEL_PATH ?? '(default)',
            image_stats: imageStats,
            next_steps: [
                { tool: 'ocr_vlm_process', description: 'Process pending VLM images' },
                { tool: 'ocr_image_pending', description: 'List images awaiting VLM processing' },
            ],
        }));
    }
    catch (error) {
        return handleError(error);
    }
}
// ===============================================================================
// TOOL DEFINITIONS FOR MCP REGISTRATION
// ===============================================================================
/**
 * Local VLM tools collection for MCP server registration
 */
export const vlmLocalTools = {
    ocr_vlm_describe: {
        description: '[PROCESSING] Use to generate a detailed AI description of a single image using local Chandra VLM. Returns multi-paragraph description with embedding. Requires image_id from database.',
        inputSchema: {
            image_id: z.string().min(1).describe('Image ID from database (use ocr_image_list to find)'),
            context_text: z.string().optional().describe('Surrounding text context from document'),
        },
        handler: handleVLMDescribe,
    },
    ocr_vlm_process: {
        description: '[PROCESSING] Use to run local VLM analysis on document images. Pass document_id for one document, omit for all pending. Uses Chandra local GPU.',
        inputSchema: {
            document_id: z.string().optional().describe('Document ID (omit to process all pending)'),
            batch_size: z.number().int().min(1).max(20).default(5).describe('Images per batch'),
            skip_processed: z
                .boolean()
                .default(true)
                .describe('Skip images that already have VLM descriptions'),
        },
        handler: handleVLMProcess,
    },
    ocr_vlm_status: {
        description: '[STATUS] Use to check VLM processing status (model info, processed/pending/failed image counts). Returns local Chandra VLM status. Use to diagnose VLM failures.',
        inputSchema: {},
        handler: handleVLMStatus,
    },
};
//# sourceMappingURL=vlm-local.js.map