/**
 * NomicEmbeddingClient - TypeScript bridge to python/embedding_worker.py (Daemon Mode)
 *
 * Spawns python/embedding_worker.py in daemon mode (--daemon).
 * Model is loaded ONCE on first use and reused across all documents.
 * Call destroy() after batch processing to free GPU VRAM.
 *
 * CP-004: Local GPU inference ONLY - throws EmbeddingError on GPU unavailable.
 * NO cloud fallback, NO CPU fallback. FAIL FAST with robust error logging.
 *
 * @module services/embedding/nomic
 */
import { spawn, execFileSync } from 'child_process';
import { createInterface } from 'readline';
import path from 'path';
import { fileURLToPath } from 'url';
import { state } from '../../server/state.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export class EmbeddingError extends Error {
    code;
    details;
    constructor(message, code, details) {
        super(message);
        this.code = code;
        this.details = details;
        this.name = 'EmbeddingError';
        Error.captureStackTrace?.(this, EmbeddingError);
    }
}
export const EMBEDDING_DIM = 768;
export const MODEL_NAME = 'nomic-embed-text-v1.5';
export const MODEL_VERSION = '1.5.0';
export const DEFAULT_BATCH_SIZE = 64;
/** Track all active embedding processes for cleanup on server shutdown. */
const _activeEmbeddingProcesses = new Set();
/** Kill all active embedding worker processes. */
export function killAllEmbeddingWorkers() {
    for (const proc of _activeEmbeddingProcesses) {
        try {
            proc.kill('SIGKILL');
        }
        catch {
            /* already dead */
        }
    }
    _activeEmbeddingProcesses.clear();
}
/** Daemon startup timeout: 120s for model loading */
const DAEMON_STARTUP_TIMEOUT_MS = 120_000;
/** Per-request timeout: 5 minutes (CUDA hang protection) */
const REQUEST_TIMEOUT_MS = 300_000;
export class NomicEmbeddingClient {
    workerPath;
    pythonPath;
    /** Daemon process state */
    _daemonProcess = null;
    _daemonReadline = null;
    _daemonReady = false;
    _daemonStarting = null;
    _pendingResolve = null;
    _pendingReject = null;
    /** Simple promise-based lock for serial access to daemon */
    _lockPromise = Promise.resolve();
    _lastDevice = 'unknown';
    constructor(options) {
        this.workerPath =
            options?.workerPath ?? path.resolve(__dirname, '../../../python/embedding_worker.py');
        this.pythonPath = options?.pythonPath ??
            (process.platform === 'win32' ? 'python' : 'python3');
    }
    /**
     * Maximum chunks per daemon request to prevent memory issues.
     */
    static MAX_CHUNKS_PER_CALL = 100;
    getEffectiveBatchSize(callerBatchSize) {
        const configBatchSize = state.config?.embeddingBatchSize;
        return configBatchSize && configBatchSize > 0 ? configBatchSize : callerBatchSize;
    }
    getEffectiveDevice() {
        return state.config?.embeddingDevice || undefined;
    }
    // ═══════════════════════════════════════════════════════════════════
    // DAEMON LIFECYCLE
    // ═══════════════════════════════════════════════════════════════════
    /** Acquire exclusive access to the daemon for one request. */
    async withLock(fn) {
        let releaseFn;
        const myTurn = new Promise(resolve => { releaseFn = resolve; });
        const previousTurn = this._lockPromise;
        this._lockPromise = myTurn;
        await previousTurn;
        try {
            return await fn();
        }
        finally {
            releaseFn();
        }
    }
    /** Ensure daemon is running and ready. Spawns on first call, restarts on crash. */
    async ensureDaemon() {
        if (this._daemonReady && this._daemonProcess && !this._daemonProcess.killed) {
            return;
        }
        if (this._daemonStarting) {
            await this._daemonStarting;
            return;
        }
        this._daemonStarting = this._spawnDaemon();
        try {
            await this._daemonStarting;
        }
        finally {
            this._daemonStarting = null;
        }
    }
    /** Spawn the Python daemon process and wait for the ready signal. */
    _spawnDaemon() {
        return new Promise((resolve, reject) => {
            this._cleanupDaemon();
            // Pre-flight: verify Python is available
            try {
                execFileSync(this.pythonPath, ['--version'], { timeout: 5000, stdio: 'pipe' });
            }
            catch (err) {
                reject(new EmbeddingError(`Python not found at '${this.pythonPath}'. Embedding requires Python 3.10+ with sentence-transformers. ` +
                    `Error: ${err instanceof Error ? err.message : String(err)}`, 'WORKER_ERROR'));
                return;
            }
            const args = ['-u', this.workerPath, '--daemon'];
            const device = this.getEffectiveDevice();
            if (device) {
                args.push('--device', device);
            }
            console.error(`[EmbeddingDaemon] Starting daemon: ${this.pythonPath} ${args.join(' ')}`);
            const proc = spawn(this.pythonPath, args, {
                stdio: ['pipe', 'pipe', 'pipe'],
                env: { ...process.env },
            });
            this._daemonProcess = proc;
            _activeEmbeddingProcesses.add(proc);
            // Forward stderr with prefix
            proc.stderr?.on('data', (chunk) => {
                const lines = chunk.toString().split('\n');
                for (const line of lines) {
                    if (line.trim()) {
                        process.stderr.write(`[EmbeddingDaemon] ${line}\n`);
                    }
                }
            });
            // Set up readline on stdout for line-based JSON
            const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
            this._daemonReadline = rl;
            rl.on('line', (line) => {
                const trimmed = line.trim();
                if (!trimmed)
                    return;
                if (this._pendingResolve) {
                    const resolveFn = this._pendingResolve;
                    this._pendingResolve = null;
                    this._pendingReject = null;
                    resolveFn(trimmed);
                }
            });
            proc.on('exit', (code, signal) => {
                _activeEmbeddingProcesses.delete(proc);
                this._daemonReady = false;
                this._daemonProcess = null;
                if (this._pendingReject) {
                    const rejectFn = this._pendingReject;
                    this._pendingReject = null;
                    this._pendingResolve = null;
                    rejectFn(new EmbeddingError(`Embedding daemon exited unexpectedly (code=${code}, signal=${signal})`, 'WORKER_ERROR'));
                }
                console.error(`[EmbeddingDaemon] Process exited (code=${code}, signal=${signal})`);
            });
            // Wait for ready signal with timeout
            const timer = setTimeout(() => {
                this._cleanupDaemon();
                reject(new EmbeddingError(`Embedding daemon startup timeout after ${DAEMON_STARTUP_TIMEOUT_MS}ms. Model may have failed to load.`, 'WORKER_ERROR'));
            }, DAEMON_STARTUP_TIMEOUT_MS);
            // The first line from daemon is the ready signal
            this._pendingResolve = (line) => {
                clearTimeout(timer);
                try {
                    const ready = JSON.parse(line);
                    if (ready.status !== 'ready') {
                        reject(new EmbeddingError(`Daemon sent unexpected first message: ${line.substring(0, 200)}`, 'WORKER_ERROR'));
                        return;
                    }
                    this._daemonReady = true;
                    this._lastDevice = ready.device ?? 'unknown';
                    console.error(`[EmbeddingDaemon] Ready on ${ready.device} in ${ready.load_time_ms}ms (${ready.model} ${ready.model_version})`);
                    resolve();
                }
                catch (err) {
                    reject(new EmbeddingError(`Failed to parse daemon ready signal: ${err instanceof Error ? err.message : String(err)}`, 'PARSE_ERROR', { line: line.substring(0, 500) }));
                }
            };
            this._pendingReject = (err) => {
                clearTimeout(timer);
                reject(err);
            };
        });
    }
    /** Send a JSON request to daemon and wait for JSON response. */
    sendRequest(request) {
        return new Promise((resolve, reject) => {
            if (!this._daemonProcess || !this._daemonProcess.stdin) {
                reject(new EmbeddingError('Daemon not running', 'WORKER_ERROR'));
                return;
            }
            const timer = setTimeout(() => {
                if (this._pendingReject) {
                    this._pendingReject = null;
                    this._pendingResolve = null;
                }
                reject(new EmbeddingError(`Embedding request timeout after ${REQUEST_TIMEOUT_MS}ms`, 'WORKER_ERROR'));
            }, REQUEST_TIMEOUT_MS);
            this._pendingResolve = (line) => {
                clearTimeout(timer);
                try {
                    const parsed = JSON.parse(line);
                    resolve(parsed);
                }
                catch (err) {
                    reject(new EmbeddingError(`Failed to parse daemon response: ${err instanceof Error ? err.message : String(err)}`, 'PARSE_ERROR', { line: line.substring(0, 500) }));
                }
            };
            this._pendingReject = (err) => {
                clearTimeout(timer);
                reject(err);
            };
            this._daemonProcess.stdin.write(JSON.stringify(request) + '\n');
        });
    }
    _cleanupDaemon() {
        if (this._daemonReadline) {
            this._daemonReadline.close();
            this._daemonReadline = null;
        }
        if (this._daemonProcess) {
            _activeEmbeddingProcesses.delete(this._daemonProcess);
            try {
                this._daemonProcess.kill('SIGKILL');
            }
            catch {
                /* already dead */
            }
            this._daemonProcess = null;
        }
        this._daemonReady = false;
        this._pendingResolve = null;
        this._pendingReject = null;
    }
    // ═══════════════════════════════════════════════════════════════════
    // PUBLIC API
    // ═══════════════════════════════════════════════════════════════════
    async embedChunks(chunks, batchSize = DEFAULT_BATCH_SIZE) {
        if (chunks.length === 0) {
            return [];
        }
        // For large chunk counts, process in sub-batches
        if (chunks.length > NomicEmbeddingClient.MAX_CHUNKS_PER_CALL) {
            return this.embedChunksInBatches(chunks, batchSize);
        }
        return this.embedChunksSingle(chunks, batchSize);
    }
    async embedChunksInBatches(chunks, batchSize) {
        const allEmbeddings = [];
        const maxPerCall = NomicEmbeddingClient.MAX_CHUNKS_PER_CALL;
        for (let i = 0; i < chunks.length; i += maxPerCall) {
            const batch = chunks.slice(i, i + maxPerCall);
            const batchNum = Math.floor(i / maxPerCall) + 1;
            const totalBatches = Math.ceil(chunks.length / maxPerCall);
            console.error(`[EMBED] Processing batch ${batchNum}/${totalBatches} (${batch.length} chunks)`);
            const embeddings = await this.embedChunksSingle(batch, batchSize);
            allEmbeddings.push(...embeddings);
        }
        return allEmbeddings;
    }
    async embedChunksSingle(chunks, batchSize) {
        return this.withLock(async () => {
            await this.ensureDaemon();
            const effectiveBatchSize = this.getEffectiveBatchSize(batchSize);
            const result = await this.sendRequest({
                action: 'embed_chunks',
                chunks,
                batch_size: effectiveBatchSize,
            });
            if (!result.success) {
                throw new EmbeddingError(result.error ?? 'Embedding generation failed with no error message', this.classifyError(result.error), {
                    count: chunks.length,
                    batchSize: effectiveBatchSize,
                    device: result.device,
                    elapsed_ms: result.elapsed_ms,
                });
            }
            // Validate output dimensions
            for (let i = 0; i < result.embeddings.length; i++) {
                if (result.embeddings[i].length !== EMBEDDING_DIM) {
                    throw new EmbeddingError(`Embedding ${i} has wrong dimensions: ${result.embeddings[i].length}, expected ${EMBEDDING_DIM}`, 'EMBEDDING_FAILED', { index: i, actualDim: result.embeddings[i].length });
                }
            }
            this._lastDevice = result.device ?? 'unknown';
            return result.embeddings.map((e) => new Float32Array(e));
        });
    }
    getLastDevice() {
        return this._lastDevice;
    }
    async embedQuery(query) {
        if (!query || query.trim().length === 0) {
            throw new EmbeddingError('Query cannot be empty', 'EMBEDDING_FAILED', { query });
        }
        return this.withLock(async () => {
            await this.ensureDaemon();
            const result = await this.sendRequest({
                action: 'embed_query',
                query,
            });
            if (!result.success) {
                throw new EmbeddingError(result.error ?? 'Query embedding failed with no error message', this.classifyError(result.error), { query: query.substring(0, 100), device: result.device });
            }
            if (result.embedding.length !== EMBEDDING_DIM) {
                throw new EmbeddingError(`Query embedding has wrong dimensions: ${result.embedding.length}, expected ${EMBEDDING_DIM}`, 'EMBEDDING_FAILED', { actualDim: result.embedding.length });
            }
            return new Float32Array(result.embedding);
        });
    }
    /**
     * Destroy the daemon process, freeing GPU VRAM.
     * Call this after batch processing is complete.
     */
    destroy() {
        if (this._daemonProcess && this._daemonProcess.stdin) {
            try {
                // Send shutdown command so Python can free CUDA memory gracefully
                this._daemonProcess.stdin.write(JSON.stringify({ action: 'shutdown' }) + '\n');
            }
            catch {
                /* stdin may be closed */
            }
        }
        // Give it a moment to clean up, then force kill
        setTimeout(() => {
            this._cleanupDaemon();
        }, 2000);
        console.error('[EmbeddingDaemon] Destroyed — GPU VRAM freed');
    }
    /**
     * Async destroy — waits for daemon to acknowledge shutdown and free VRAM.
     * Use this when loading another GPU model after embedding.
     */
    async destroyAndWait(timeoutMs = 5000) {
        if (!this._daemonProcess || !this._daemonProcess.stdin) {
            this._cleanupDaemon();
            return;
        }
        return new Promise((resolve) => {
            const timer = setTimeout(() => {
                console.error('[EmbeddingDaemon] Shutdown timeout, force killing');
                this._cleanupDaemon();
                resolve();
            }, timeoutMs);
            // Listen for shutdown acknowledgment
            this._pendingResolve = () => {
                clearTimeout(timer);
                this._cleanupDaemon();
                resolve();
            };
            try {
                this._daemonProcess.stdin.write(JSON.stringify({ action: 'shutdown' }) + '\n');
            }
            catch {
                clearTimeout(timer);
                this._cleanupDaemon();
                resolve();
            }
        });
    }
    classifyError(error) {
        if (!error)
            return 'EMBEDDING_FAILED';
        const lower = error.toLowerCase();
        if (lower.includes('gpu') || lower.includes('cuda') || lower.includes('no device')) {
            return 'GPU_NOT_AVAILABLE';
        }
        if (lower.includes('model not found') || lower.includes('no such file')) {
            return 'MODEL_NOT_FOUND';
        }
        return 'EMBEDDING_FAILED';
    }
}
let _client = null;
export function getEmbeddingClient() {
    if (!_client) {
        _client = new NomicEmbeddingClient();
    }
    return _client;
}
//# sourceMappingURL=nomic.js.map