/**
 * OCR Processing Orchestrator
 *
 * Complete pipeline: Document -> OCR -> Provenance -> Store -> Status Update
 * FAIL-FAST: No fallbacks, errors propagate immediately
 */
import { v4 as uuidv4 } from 'uuid';
import { getMarkerClient } from './marker.js';
import { OCRError } from './errors.js';
import { ProvenanceType } from '../../models/provenance.js';
import { getOcrMaxConcurrent } from './config-local.js';
import { getEmbeddingClient } from '../embedding/nomic.js';
import { killAllVLMWorkers } from '../vlm/chandra.js';
/**
 * Marker-pdf version for provenance records
 */
const MARKER_VERSION = '1.10.2';
/**
 * Default output extras requested when none are explicitly provided.
 * extract_links and table_row_bboxes are always useful for document intelligence.
 * track_changes is NOT included by default since it is only relevant for
 * DOCX files with revision tracking enabled.
 */
const DEFAULT_EXTRAS = ['extract_links', 'table_row_bboxes'];
function parseMaxConcurrent() {
    return getOcrMaxConcurrent();
}
export class OCRProcessor {
    client;
    db;
    maxConcurrent;
    defaultMode;
    constructor(db, config = {}) {
        this.db = db;
        this.client = getMarkerClient(config);
        this.maxConcurrent = config.maxConcurrent ?? parseMaxConcurrent();
        this.defaultMode = config.defaultMode ?? 'balanced';
    }
    /**
     * Process single document through OCR
     *
     * Pipeline:
     * 1. Get document from database (FAIL if not found)
     * 2. Update status to 'processing'
     * 3. Call Marker OCR via Python worker
     * 4. Create OCR_RESULT provenance record
     * 5. Store OCR result in database
     * 6. Update document status to 'complete'
     *
     * On transient failure: Keep status='pending' for retry (timeout, GPU OOM, insufficient balance)
     * On permanent failure: Update status to 'failed' (file errors, unclassified errors)
     */
    async processDocument(documentId, mode, ocrOptions) {
        const ocrMode = mode ?? this.defaultMode;
        const startTime = Date.now();
        // 1. Get document (FAIL-FAST: throw if not found)
        const document = this.db.getDocument(documentId);
        if (!document) {
            throw new OCRError(`Document not found: ${documentId}`, 'OCR_FILE_ERROR');
        }
        // 2. Update status to 'processing'
        this.db.updateDocumentStatus(documentId, 'processing');
        try {
            // 3. Generate provenance ID and call OCR (with 1 retry on timeout)
            const ocrProvenanceId = uuidv4();
            let ocrResult;
            let images;
            let jsonBlocks = null;
            let metadata = null;
            let pageOffsets = [];
            let extractionJson = null;
            let docTitle = null;
            let docAuthor = null;
            let docSubject = null;
            // Apply default extras when none explicitly provided
            const effectiveOptions = ocrOptions
                ? { ...ocrOptions, extras: ocrOptions.extras ?? [...DEFAULT_EXTRAS] }
                : { extras: [...DEFAULT_EXTRAS] };
            for (let attempt = 1; attempt <= 2; attempt++) {
                try {
                    const response = await this.client.processDocument(document.file_path, documentId, ocrProvenanceId, ocrMode, effectiveOptions);
                    ocrResult = response.result;
                    images = response.images;
                    jsonBlocks = response.jsonBlocks;
                    metadata = response.metadata;
                    pageOffsets = response.pageOffsets;
                    extractionJson = response.extractionJson;
                    docTitle = response.docTitle;
                    docAuthor = response.docAuthor;
                    docSubject = response.docSubject;
                    break;
                }
                catch (error) {
                    // Only retry OCR_TIMEOUT (network/processing timeout — may succeed on retry).
                    // OCR_MODEL_ERROR is environmental (missing deps, daemon crash) — retrying
                    // immediately is pointless and wastes time. Let it propagate to the outer
                    // catch which correctly classifies it as transient (keeps doc 'pending').
                    if (attempt === 1 && error instanceof OCRError && error.category === 'OCR_TIMEOUT') {
                        console.error(`[WARN] OCR ${error.category} on attempt 1 for ${documentId}, retrying...`);
                        continue;
                    }
                    throw error;
                }
            }
            // TypeScript: guaranteed assigned after loop or thrown
            ocrResult = ocrResult;
            images = images;
            // 4. Create OCR_RESULT provenance record
            const provenance = this.createOCRProvenance(ocrProvenanceId, document, ocrResult, ocrMode);
            this.db.insertProvenance(provenance);
            // 5. Store OCR result
            this.db.insertOCRResult(ocrResult);
            // 6. Update document status
            this.db.updateDocumentOCRComplete(documentId, ocrResult.page_count, ocrResult.processing_completed_at);
            // Capture image count for logging
            const imageCount = Object.keys(images).length;
            if (imageCount > 0) {
                console.error(`[INFO] Captured ${imageCount} images from Marker OCR`);
            }
            return {
                success: true,
                documentId,
                ocrResultId: ocrResult.id,
                provenanceId: ocrProvenanceId,
                pageCount: ocrResult.page_count,
                textLength: ocrResult.text_length,
                durationMs: Date.now() - startTime,
                images: imageCount > 0 ? images : undefined,
                jsonBlocks,
                metadata,
                pageOffsets: pageOffsets.length > 0 ? pageOffsets : undefined,
                extractionJson: extractionJson ?? undefined,
                docTitle: docTitle ?? undefined,
                docAuthor: docAuthor ?? undefined,
                docSubject: docSubject ?? undefined,
            };
        }
        catch (error) {
            const errorMsg = error instanceof Error ? error.message : String(error);
            // CONCURRENCY FIX: Do NOT update document status here. The caller (ingestion.ts)
            // is the sole owner of status transitions. Setting status here caused double-writes:
            // processor set 'failed', then ingestion's transient-error logic overrode to 'pending',
            // masking the real error and creating retry loops.
            console.error(`[ERROR] OCR processing error for ${documentId}: ${errorMsg}`);
            // Re-throw as OCRError if not already one (FAIL-FAST: callers must handle)
            if (error instanceof OCRError) {
                throw error;
            }
            // CONCURRENCY FIX: Database integrity errors (FK violations, unique constraints)
            // are PERMANENT — they indicate a data consistency problem, not a transient
            // environment issue. Wrapping them as OCR_MODEL_ERROR (transient) caused infinite
            // retry loops because the document kept reverting to 'pending'.
            const lowerMsg = errorMsg.toLowerCase();
            if (lowerMsg.includes('foreign key') ||
                lowerMsg.includes('unique constraint') ||
                lowerMsg.includes('not null constraint') ||
                lowerMsg.includes('sqlite_constraint') ||
                lowerMsg.includes('constraint failed') ||
                lowerMsg.includes('integrity')) {
                throw new OCRError(`Database integrity error for ${documentId}: ${errorMsg}`, 'OCR_FILE_ERROR');
            }
            throw new OCRError(`OCR processing failed for ${documentId}: ${errorMsg}`, 'OCR_MODEL_ERROR');
        }
    }
    /**
     * Process all pending documents.
     *
     * H-2 fix: Before processing, recover any documents stuck in 'processing'
     * status for longer than 30 minutes (indicates a prior server crash).
     * These are reset to 'pending' so they get re-processed.
     */
    async processPending(mode) {
        const startTime = Date.now();
        const ocrMode = mode ?? this.defaultMode;
        // H-2: Recover stale 'processing' documents (crashed mid-OCR)
        this.recoverStaleProcessingDocuments();
        const pending = this.db.listDocuments({ status: 'pending' });
        if (pending.length === 0) {
            return {
                processed: 0,
                failed: 0,
                remaining: 0,
                totalDurationMs: 0,
                results: [],
            };
        }
        console.error(`[OCRProcessor] Processing ${pending.length} pending documents (maxConcurrent=${this.maxConcurrent})`);
        const results = [];
        // Process in batches for concurrency control
        for (let i = 0; i < pending.length; i += this.maxConcurrent) {
            const batch = pending.slice(i, i + this.maxConcurrent);
            const batchResults = await Promise.all(batch.map(async (doc) => {
                try {
                    return await this.processDocument(doc.id, ocrMode);
                }
                catch (error) {
                    // processDocument already set status before throwing (pending for transient, failed for permanent)
                    const errorMsg = error instanceof Error ? error.message : String(error);
                    return {
                        success: false,
                        documentId: doc.id,
                        error: errorMsg,
                        durationMs: 0,
                    };
                }
            }));
            results.push(...batchResults);
        }
        const processed = results.filter((r) => r.success).length;
        const failed = results.length - processed;
        const remaining = this.db.listDocuments({ status: 'pending' }).length;
        // Free ALL GPU VRAM after batch processing completes.
        // Models are loaded on-demand during processing and must be released
        // so VRAM is available for other tasks (VLM, user GPU workloads, etc.).
        console.error(`[OCRProcessor] Batch complete (${processed} processed, ${failed} failed). Freeing GPU VRAM...`);
        try {
            // Destroy Marker OCR daemon (frees ~4GB VRAM)
            await this.client.destroyAndWait(5000);
            console.error('[OCRProcessor] Marker OCR daemon destroyed');
        }
        catch (err) {
            console.error(`[OCRProcessor] Marker cleanup error: ${err instanceof Error ? err.message : String(err)}`);
        }
        try {
            // Destroy embedding daemon (frees ~1GB VRAM)
            await getEmbeddingClient().destroyAndWait(5000);
            console.error('[OCRProcessor] Embedding daemon destroyed');
        }
        catch (err) {
            console.error(`[OCRProcessor] Embedding cleanup error: ${err instanceof Error ? err.message : String(err)}`);
        }
        try {
            // Kill VLM workers (frees ~10.6GB VRAM)
            killAllVLMWorkers();
            console.error('[OCRProcessor] VLM workers killed');
        }
        catch (err) {
            console.error(`[OCRProcessor] VLM cleanup error: ${err instanceof Error ? err.message : String(err)}`);
        }
        console.error('[OCRProcessor] All GPU models unloaded — VRAM freed');
        return {
            processed,
            failed,
            remaining,
            totalDurationMs: Date.now() - startTime,
            results,
        };
    }
    /**
     * H-2: Recover documents stuck in 'processing' status after a server crash.
     * Any document that has been 'processing' for longer than 30 minutes is
     * assumed to be orphaned from a crash and is reset to 'pending'.
     */
    recoverStaleProcessingDocuments() {
        const conn = this.db.getConnection();
        const staleRows = conn
            .prepare(`SELECT id FROM documents WHERE status = 'processing'
         AND modified_at < datetime('now', '-30 minutes')`)
            .all();
        for (const row of staleRows) {
            console.error(`[WARN] Recovering stale 'processing' document ${row.id} (stuck >30min, likely server crash). Cleaning derived data and resetting to 'pending'.`);
            try {
                this.db.cleanDocumentDerivedData(row.id);
            }
            catch (cleanupError) {
                console.error(`[WARN] Cleanup failed for stale document ${row.id}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
            }
            this.db.updateDocumentStatus(row.id, 'pending');
        }
        if (staleRows.length > 0) {
            console.error(`[INFO] Recovered ${staleRows.length} stale 'processing' document(s) to 'pending' status.`);
        }
    }
    /**
     * Create OCR_RESULT provenance record
     */
    createOCRProvenance(id, document, ocrResult, mode) {
        const now = new Date().toISOString();
        return {
            id,
            type: ProvenanceType.OCR_RESULT,
            created_at: now,
            processed_at: ocrResult.processing_completed_at,
            source_file_created_at: null,
            source_file_modified_at: document.modified_at,
            source_type: 'OCR',
            source_path: document.file_path,
            source_id: document.provenance_id,
            root_document_id: document.provenance_id,
            location: null,
            content_hash: ocrResult.content_hash,
            input_hash: document.file_hash,
            file_hash: document.file_hash,
            processor: 'marker-ocr',
            processor_version: MARKER_VERSION,
            processing_params: {
                mode,
                output_format: 'markdown,json',
                request_id: ocrResult.datalab_request_id,
                paginate: true,
            },
            processing_duration_ms: ocrResult.processing_duration_ms,
            processing_quality_score: ocrResult.parse_quality_score,
            parent_id: document.provenance_id,
            parent_ids: JSON.stringify([document.provenance_id]),
            chain_depth: 1,
            chain_path: JSON.stringify(['DOCUMENT', 'OCR_RESULT']),
        };
    }
}
//# sourceMappingURL=processor.js.map