/**
 * EmbeddingService - Embedding generation orchestrator
 *
 * CP-001: Complete provenance (EMBEDDING records, chain_depth=3)
 * CP-002: original_text denormalized in every embedding
 * CP-004: Local GPU only - NO cloud fallback
 *
 * ATOMIC: All operations succeed or none are stored. FAIL FAST.
 *
 * @module services/embedding/embedder
 */
import { v4 as uuidv4 } from 'uuid';
import { getEmbeddingClient, EmbeddingError, EMBEDDING_DIM, MODEL_NAME, MODEL_VERSION, DEFAULT_BATCH_SIZE, } from './nomic.js';
import { computeHash } from '../../utils/hash.js';
import { normalizeForEmbedding } from '../chunking/text-normalizer.js';
const CHAIN_PATH = JSON.stringify(['DOCUMENT', 'OCR_RESULT', 'CHUNK', 'EMBEDDING']);
/** Section-aware embedding processor version */
const SECTION_AWARE_VERSION = '1.5.0-section';
/**
 * Build a section-aware context prefix from chunk metadata.
 * This prefix is prepended to the chunk text before embedding to make
 * embeddings section-aware WITHOUT changing stored text.
 *
 * @param chunk - Chunk with section/heading/content_types metadata
 * @returns Prefix string (may be empty if no relevant metadata)
 */
export function buildSectionPrefix(chunk) {
    const parts = [];
    if (chunk.section_path) {
        parts.push(`[Section: ${chunk.section_path}]`);
    }
    else if (chunk.heading_context) {
        parts.push(`[Heading: ${chunk.heading_context}]`);
    }
    if (chunk.content_types) {
        try {
            const types = JSON.parse(chunk.content_types);
            if (types.includes('table'))
                parts.push('[Table]');
            if (types.includes('code'))
                parts.push('[Code]');
        }
        catch (error) {
            console.error(`[embedder] Failed to parse content_types JSON for chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    return parts.length > 0 ? parts.join(' ') + ' ' : '';
}
export class EmbeddingService {
    client;
    batchSize;
    constructor(options) {
        this.client = options?.client ?? getEmbeddingClient();
        this.batchSize = options?.batchSize ?? DEFAULT_BATCH_SIZE;
    }
    async embedDocumentChunks(db, vectorService, chunks, documentInfo) {
        if (chunks.length === 0) {
            return { success: true, embeddingIds: [], provenanceIds: [], totalChunks: 0, elapsedMs: 0 };
        }
        const startMs = Date.now();
        const vectors = await this.client.embedChunks(chunks.map((c) => buildSectionPrefix(c) + normalizeForEmbedding(c.text)), this.batchSize);
        if (vectors.length !== chunks.length) {
            throw new EmbeddingError(`Vector count mismatch: got ${vectors.length}, expected ${chunks.length}`, 'EMBEDDING_FAILED', { vectorCount: vectors.length, chunkCount: chunks.length });
        }
        // Get actual device from last successful embedding operation
        const actualDevice = this.client.getLastDevice();
        const embeddingElapsedMs = Date.now() - startMs;
        // Use Math.max(1, ...) to ensure at least 1ms when embedding actually occurred,
        // since sub-millisecond per-embedding times round to 0 with fast batch inference
        const perEmbeddingMs = chunks.length > 0 && embeddingElapsedMs > 0
            ? Math.max(1, Math.round(embeddingElapsedMs / chunks.length))
            : null;
        const result = db.transaction(() => {
            const embeddingIds = [];
            const provenanceIds = [];
            // H-7/M-16: Sub-batch vector storage to avoid accumulating all Float32Array vectors in memory
            const VECTOR_SUB_BATCH = 50;
            let vectorBatch = [];
            for (let i = 0; i < chunks.length; i++) {
                const chunk = chunks[i];
                const vector = vectors[i];
                const embeddingId = uuidv4();
                const now = new Date().toISOString();
                const provenanceId = this.createProvenance(db, chunk, documentInfo, actualDevice, perEmbeddingMs);
                provenanceIds.push(provenanceId);
                const embedding = {
                    id: embeddingId,
                    chunk_id: chunk.id,
                    image_id: null, // Text embeddings don't have an image
                    extraction_id: null, // Text embeddings don't have an extraction
                    document_id: documentInfo.documentId,
                    original_text: chunk.text,
                    original_text_length: chunk.text.length,
                    source_file_path: documentInfo.filePath,
                    source_file_name: documentInfo.fileName,
                    source_file_hash: documentInfo.fileHash,
                    page_number: chunk.page_number,
                    page_range: chunk.page_range,
                    character_start: chunk.character_start,
                    character_end: chunk.character_end,
                    chunk_index: chunk.chunk_index,
                    total_chunks: chunks.length,
                    model_name: MODEL_NAME,
                    model_version: MODEL_VERSION,
                    task_type: 'search_document',
                    inference_mode: 'local',
                    gpu_device: actualDevice,
                    provenance_id: provenanceId,
                    content_hash: computeHash(chunk.text),
                    generation_duration_ms: null,
                };
                db.insertEmbedding(embedding);
                embeddingIds.push(embeddingId);
                vectorBatch.push({ embeddingId, vector });
                // H-7/M-16: Flush vector sub-batch to avoid accumulating all vectors in memory
                if (vectorBatch.length >= VECTOR_SUB_BATCH) {
                    vectorService.batchStoreVectors(vectorBatch);
                    vectorBatch = [];
                }
                db.updateChunkEmbeddingStatus(chunk.id, 'complete', now);
            }
            // Flush remaining vectors
            if (vectorBatch.length > 0) {
                vectorService.batchStoreVectors(vectorBatch);
            }
            return { embeddingIds, provenanceIds };
        });
        return {
            success: true,
            embeddingIds: result.embeddingIds,
            provenanceIds: result.provenanceIds,
            totalChunks: chunks.length,
            elapsedMs: Date.now() - startMs,
        };
    }
    async embedSearchQuery(query) {
        return this.client.embedQuery(query);
    }
    async processPendingChunks(db, vectorService, documentInfo) {
        const allChunks = db.getChunksByDocumentId(documentInfo.documentId);
        const pendingChunks = allChunks.filter((c) => c.embedding_status === 'pending');
        if (pendingChunks.length === 0) {
            return { success: true, embeddingIds: [], provenanceIds: [], totalChunks: 0, elapsedMs: 0 };
        }
        return this.embedDocumentChunks(db, vectorService, pendingChunks, documentInfo);
    }
    createProvenance(db, chunk, documentInfo, device, perEmbeddingDurationMs) {
        const provenanceId = uuidv4();
        const now = new Date().toISOString();
        const chunkProv = db.getProvenance(chunk.provenance_id);
        const existingParents = chunkProv?.parent_ids
            ? JSON.parse(chunkProv.parent_ids)
            : [];
        const parentIds = [...existingParents, chunk.provenance_id];
        const record = {
            id: provenanceId,
            type: '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: chunk.provenance_id,
            root_document_id: documentInfo.documentProvenanceId,
            location: {
                chunk_index: chunk.chunk_index,
                character_start: chunk.character_start,
                character_end: chunk.character_end,
                page_number: chunk.page_number ?? undefined,
            },
            content_hash: computeHash(chunk.text),
            input_hash: chunk.text_hash,
            file_hash: documentInfo.fileHash,
            processor: MODEL_NAME,
            processor_version: SECTION_AWARE_VERSION,
            processing_params: {
                dimensions: EMBEDDING_DIM,
                task_type: 'search_document',
                inference_mode: 'local',
                device,
                dtype: 'float16',
                batch_size: this.batchSize,
                section_aware: true,
            },
            processing_duration_ms: perEmbeddingDurationMs,
            processing_quality_score: null,
            parent_id: chunk.provenance_id,
            parent_ids: JSON.stringify(parentIds),
            chain_depth: 3,
            chain_path: CHAIN_PATH,
        };
        db.insertProvenance(record);
        return provenanceId;
    }
}
let _service = null;
export function getEmbeddingService() {
    if (!_service) {
        _service = new EmbeddingService();
    }
    return _service;
}
export function resetEmbeddingService() {
    _service = null;
}
export { EmbeddingError };
//# sourceMappingURL=embedder.js.map