/**
 * Chandra VLM Client - Local GPU Image Analysis (Daemon Mode)
 *
 * Spawns python/vlm_worker_local.py in daemon mode (--daemon).
 * Model is loaded ONCE on first use and reused across all requests.
 * Uses ChandraClient (local Chandra OCR 2 v0.2.0) for 100% local GPU processing.
 * NO cloud API calls, NO fallbacks.
 *
 * Return types are IDENTICAL to VLMService for drop-in replacement.
 *
 * @module services/vlm/chandra
 */
import { spawn } from 'child_process';
import { createInterface } from 'readline';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/** Daemon startup timeout: 120s for model loading (~10.6GB) */
const DAEMON_STARTUP_TIMEOUT_MS = 120_000;
/**
 * Track all active VLM child processes for cleanup on server shutdown.
 * Prevents orphaned Chandra workers that survive server restarts.
 */
const _activeVLMProcesses = new Set();
/**
 * Kill all active VLM worker processes.
 * Called on process exit to prevent orphaned GPU-consuming workers.
 */
export function killAllVLMWorkers() {
    for (const proc of _activeVLMProcesses) {
        try {
            proc.kill('SIGKILL');
        }
        catch {
            /* already dead */
        }
    }
    _activeVLMProcesses.clear();
}
/**
 * ChandraClient - Local GPU VLM image analysis via Chandra OCR 2 (v0.2.0, Daemon Mode)
 *
 * Spawns a persistent Python daemon that loads the model ONCE.
 * All requests are serialized through the daemon via JSON lines on stdin/stdout.
 *
 * Provides methods for:
 * - describeImage: Detailed image analysis with structured output
 * - classifyImage: Quick categorization
 * - deepAnalyze: Extended reasoning analysis
 * - processBatch: Sequential batch processing via daemon
 */
export class ChandraClient {
    pythonPath;
    workerPath;
    method;
    batchSize;
    timeout;
    /** 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();
    constructor(config = {}) {
        this.pythonPath = config.pythonPath ??
            (process.platform === 'win32' ? 'python' : 'python3');
        this.workerPath = resolve(__dirname, '../../../python/vlm_worker_local.py');
        this.method = config.method ?? process.env.CHANDRA_METHOD ?? 'hf';
        const parsedBatchSize = config.batchSize ?? parseInt(process.env.CHANDRA_BATCH_SIZE || '4');
        this.batchSize = (Number.isNaN(parsedBatchSize) || parsedBatchSize < 1) ? 4 : parsedBatchSize;
        this.timeout = config.timeout ?? 300000; // 5 min default
        console.error(`[ChandraVLM] Initialized with timeout=${this.timeout}ms (${(this.timeout / 60000).toFixed(1)} min), daemon mode`);
    }
    /**
     * Acquire exclusive access to the daemon for one request.
     * Requests are serialized because the daemon processes one at a time.
     */
    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 another caller is already starting the daemon, wait for it
        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) => {
            // Clean up any previous daemon
            this._cleanupDaemon();
            const args = [
                '-u', // unbuffered
                this.workerPath,
                '--daemon',
                '--method', this.method,
            ];
            console.error(`[ChandraDaemon] Starting daemon: ${this.pythonPath} ${args.join(' ')}`);
            const proc = spawn(this.pythonPath, args, {
                stdio: ['pipe', 'pipe', 'pipe'],
                env: { ...process.env },
            });
            this._daemonProcess = proc;
            _activeVLMProcesses.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(`[ChandraDaemon] ${line}\n`);
                    }
                }
            });
            // Set up readline on stdout for line-based JSON reading
            const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
            this._daemonReadline = rl;
            // Handle lines from daemon
            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);
                }
            });
            // Handle daemon process exit
            proc.on('exit', (code, signal) => {
                console.error(`[ChandraDaemon] Process exited (code=${code}, signal=${signal})`);
                _activeVLMProcesses.delete(proc);
                this._daemonReady = false;
                this._daemonProcess = null;
                // If there is a pending request, reject it
                if (this._pendingReject) {
                    const rejectFn = this._pendingReject;
                    this._pendingResolve = null;
                    this._pendingReject = null;
                    rejectFn(new Error(`VLM daemon process exited unexpectedly (code=${code}, signal=${signal})`));
                }
            });
            proc.on('error', (err) => {
                console.error(`[ChandraDaemon] Process spawn error: ${err.message}`);
                _activeVLMProcesses.delete(proc);
                this._daemonReady = false;
                this._daemonProcess = null;
                if (this._pendingReject) {
                    const rejectFn = this._pendingReject;
                    this._pendingResolve = null;
                    this._pendingReject = null;
                    rejectFn(new Error(`VLM daemon failed to spawn: ${err.message}`));
                }
            });
            // Wait for the ready signal with timeout
            const startupTimer = setTimeout(() => {
                this._cleanupDaemon();
                reject(new Error(`VLM daemon failed to start within ${DAEMON_STARTUP_TIMEOUT_MS / 1000}s. ` +
                    `Model loading may have timed out or Python/Chandra may not be installed.`));
            }, DAEMON_STARTUP_TIMEOUT_MS);
            // Read the first line as the ready signal
            this._pendingResolve = (line) => {
                clearTimeout(startupTimer);
                try {
                    const parsed = JSON.parse(line);
                    if (this._isDaemonReady(parsed)) {
                        this._daemonReady = true;
                        console.error(`[ChandraDaemon] Ready (model loaded in ${parsed.load_time_ms}ms)`);
                        resolve();
                    }
                    else if (this._isErrorResponse(parsed)) {
                        this._cleanupDaemon();
                        reject(new Error(`VLM daemon startup error [${parsed.category}]: ${parsed.error}`));
                    }
                    else {
                        this._cleanupDaemon();
                        reject(new Error(`VLM daemon sent unexpected startup response: ${line.substring(0, 200)}`));
                    }
                }
                catch {
                    this._cleanupDaemon();
                    reject(new Error(`VLM daemon sent non-JSON startup output: ${line.substring(0, 200)}`));
                }
            };
            this._pendingReject = (err) => {
                clearTimeout(startupTimer);
                reject(err);
            };
        });
    }
    /**
     * Send a JSON request to the daemon and wait for a JSON response line.
     * Caller must hold the lock.
     */
    _sendRequest(request) {
        return new Promise((resolve, reject) => {
            if (!this._daemonProcess || !this._daemonProcess.stdin || this._daemonProcess.killed) {
                reject(new Error('VLM daemon is not running (stdin unavailable)'));
                return;
            }
            // Set up response handler
            this._pendingResolve = resolve;
            this._pendingReject = reject;
            // Write JSON line to stdin
            const jsonLine = JSON.stringify(request) + '\n';
            this._daemonProcess.stdin.write(jsonLine, (err) => {
                if (err) {
                    this._pendingResolve = null;
                    this._pendingReject = null;
                    reject(new Error(`Failed to write to VLM daemon stdin: ${err.message}`));
                }
            });
        });
    }
    /**
     * Wrap a promise with a timeout.
     */
    _withTimeout(promise, timeoutMs, context) {
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new Error(`VLM processing timed out after ${timeoutMs}ms (${context}). ` +
                    `To increase timeout, use ChandraClientConfig.timeout.`));
            }, timeoutMs);
            promise.then((val) => { clearTimeout(timer); resolve(val); }, (err) => { clearTimeout(timer); reject(err); });
        });
    }
    /**
     * Clean up daemon process and associated resources.
     */
    _cleanupDaemon() {
        this._daemonReady = false;
        this._pendingResolve = null;
        this._pendingReject = null;
        if (this._daemonReadline) {
            this._daemonReadline.close();
            this._daemonReadline = null;
        }
        if (this._daemonProcess) {
            _activeVLMProcesses.delete(this._daemonProcess);
            try {
                this._daemonProcess.kill('SIGKILL');
            }
            catch {
                /* already dead */
            }
            this._daemonProcess = null;
        }
    }
    _isDaemonReady(response) {
        return (typeof response === 'object' &&
            response !== null &&
            'status' in response &&
            response.status === 'ready' &&
            'model_loaded' in response);
    }
    _isErrorResponse(response) {
        return (typeof response === 'object' &&
            response !== null &&
            'error' in response &&
            'category' in response);
    }
    /**
     * Parse a JSON response line from the daemon.
     */
    _parseResponse(line) {
        try {
            return JSON.parse(line);
        }
        catch {
            throw new Error(`Failed to parse VLM daemon response as JSON: ${line.substring(0, 200)}`);
        }
    }
    /**
     * Generate detailed description of an image via daemon.
     *
     * @param imagePath - Path to the image file
     * @param options - Optional context text, prompt overrides
     * @returns VLMAnalysisResult with description and structured data
     */
    async describeImage(imagePath, options = {}) {
        return this.withLock(async () => {
            await this.ensureDaemon();
            const request = {
                action: 'analyze',
                image_path: imagePath,
                method: this.method,
                max_tokens: 2048,
            };
            // If context text provided, build a context prompt
            if (options.contextText) {
                const truncated = options.contextText.slice(0, 2000);
                request.prompt = `The following text surrounds this image in the document:\n"""\n${truncated}\n"""\n\nRelate your description to this context where relevant.`;
            }
            const requestStart = Date.now();
            let responseLine;
            try {
                responseLine = await this._withTimeout(this._sendRequest(request), this.timeout, `describeImage: ${imagePath}`);
            }
            catch (error) {
                if (error instanceof Error && error.message.includes('timed out')) {
                    console.error(`[ChandraDaemon] describeImage timeout after ${Date.now() - requestStart}ms, killing daemon`);
                    this._cleanupDaemon();
                }
                throw error;
            }
            const requestDuration = Date.now() - requestStart;
            console.error(`[ChandraVLM] describeImage completed in ${requestDuration}ms for ${imagePath}`);
            const response = this._parseResponse(responseLine);
            if (this._isErrorResponse(response)) {
                throw new Error(`VLM error [${response.category}]: ${response.error}`);
            }
            return this.parseAnalysisResult(response);
        });
    }
    /**
     * Quick classification of an image via daemon.
     *
     * @param imagePath - Path to the image file
     * @returns ImageClassification with type and complexity
     */
    async classifyImage(imagePath) {
        return this.withLock(async () => {
            await this.ensureDaemon();
            const request = {
                action: 'classify',
                image_path: imagePath,
                method: this.method,
            };
            const requestStart = Date.now();
            let responseLine;
            try {
                responseLine = await this._withTimeout(this._sendRequest(request), this.timeout, `classifyImage: ${imagePath}`);
            }
            catch (error) {
                if (error instanceof Error && error.message.includes('timed out')) {
                    console.error(`[ChandraDaemon] classifyImage timeout after ${Date.now() - requestStart}ms, killing daemon`);
                    this._cleanupDaemon();
                }
                throw error;
            }
            const requestDuration = Date.now() - requestStart;
            console.error(`[ChandraVLM] classifyImage completed in ${requestDuration}ms for ${imagePath}`);
            const response = this._parseResponse(responseLine);
            if (this._isErrorResponse(response)) {
                throw new Error(`VLM error [${response.category}]: ${response.error}`);
            }
            return this.parseClassificationResult(response);
        });
    }
    /**
     * Deep analysis using extended reasoning via daemon.
     *
     * @param imagePath - Path to the image file
     * @param contextText - Optional surrounding text context
     * @returns VLMAnalysisResult (same type as describeImage for pipeline compat)
     */
    async deepAnalyze(imagePath, contextText) {
        const deepPrompt = `You are a document analysis expert. Analyze this image thoroughly.

Step through your analysis:
1. Identify the image type and overall content
2. Examine every visible element systematically
3. Extract all text, numbers, dates, and names
4. Consider the legal or medical significance of each element
5. Note any uncertainties or ambiguities

Return as JSON:
{
  "imageType": "string",
  "primarySubject": "brief description of main subject",
  "paragraph1": "detailed visual overview (6-10 sentences)",
  "paragraph2": "comprehensive element-by-element description (8-12 sentences)",
  "paragraph3": "communication and significance (6-10 sentences)",
  "extractedText": ["all visible text"],
  "dates": ["YYYY-MM-DD"],
  "names": ["people, organizations"],
  "numbers": ["amounts, references, IDs"],
  "confidence": 0.0-1.0
}
Return ONLY valid JSON, no other text.`;
        const prompt = contextText
            ? `The following text surrounds this image:\n"""\n${contextText.slice(0, 2000)}\n"""\n\n${deepPrompt}`
            : deepPrompt;
        return this.withLock(async () => {
            await this.ensureDaemon();
            const request = {
                action: 'analyze',
                image_path: imagePath,
                method: this.method,
                max_tokens: 4096,
                prompt,
            };
            const requestStart = Date.now();
            let responseLine;
            try {
                responseLine = await this._withTimeout(this._sendRequest(request), this.timeout, `deepAnalyze: ${imagePath}`);
            }
            catch (error) {
                if (error instanceof Error && error.message.includes('timed out')) {
                    console.error(`[ChandraDaemon] deepAnalyze timeout after ${Date.now() - requestStart}ms, killing daemon`);
                    this._cleanupDaemon();
                }
                throw error;
            }
            const requestDuration = Date.now() - requestStart;
            console.error(`[ChandraVLM] deepAnalyze completed in ${requestDuration}ms for ${imagePath}`);
            const response = this._parseResponse(responseLine);
            if (this._isErrorResponse(response)) {
                throw new Error(`VLM error [${response.category}]: ${response.error}`);
            }
            return this.parseAnalysisResult(response);
        });
    }
    /**
     * Process a batch of images sequentially via daemon.
     * Each image is sent as a separate analyze request since the daemon is single-threaded.
     *
     * @param images - Array of image specs with paths and optional prompts
     * @returns Array of VLMAnalysisResult
     */
    async processBatch(images) {
        if (images.length === 0)
            return [];
        const results = [];
        for (let i = 0; i < images.length; i++) {
            const img = images[i];
            try {
                const result = await this.withLock(async () => {
                    await this.ensureDaemon();
                    const request = {
                        action: 'analyze',
                        image_path: img.imagePath,
                        method: this.method,
                        max_tokens: 2048,
                    };
                    if (img.prompt) {
                        request.prompt = img.prompt;
                    }
                    const responseLine = await this._withTimeout(this._sendRequest(request), this.timeout, `processBatch[${i}]: ${img.imagePath}`);
                    const response = this._parseResponse(responseLine);
                    if (this._isErrorResponse(response)) {
                        throw new Error(`VLM error [${response.category}]: ${response.error}`);
                    }
                    return this.parseAnalysisResult(response);
                });
                results.push(result);
            }
            catch (error) {
                console.error(`[ChandraVLM] processBatch item ${i + 1}/${images.length} failed: ${error instanceof Error ? error.message : String(error)}`);
                // Push error result to maintain array alignment
                results.push({
                    description: '',
                    analysis: {
                        imageType: 'other',
                        primarySubject: `Error: ${error instanceof Error ? error.message : String(error)}`,
                        paragraph1: `Failed to analyze image: ${error instanceof Error ? error.message : String(error)}`,
                        paragraph2: '',
                        paragraph3: '',
                        extractedText: [],
                        dates: [],
                        names: [],
                        numbers: [],
                        confidence: 0.0,
                    },
                    model: 'chandra',
                    processingTimeMs: 0,
                    tokensUsed: 0,
                });
            }
        }
        return results;
    }
    /**
     * Get service status.
     */
    getStatus() {
        return {
            model: 'chandra',
            method: this.method,
            batchSize: this.batchSize,
        };
    }
    /**
     * Parse worker JSON output into VLMAnalysisResult.
     */
    parseAnalysisResult(raw) {
        const analysis = (raw.analysis ?? {});
        return {
            description: raw.description ?? '',
            analysis: {
                imageType: analysis.imageType ?? 'unknown',
                primarySubject: analysis.primarySubject ?? '',
                paragraph1: analysis.paragraph1 ?? '',
                paragraph2: analysis.paragraph2 ?? '',
                paragraph3: analysis.paragraph3 ?? '',
                extractedText: Array.isArray(analysis.extractedText) ? analysis.extractedText : [],
                dates: Array.isArray(analysis.dates) ? analysis.dates : [],
                names: Array.isArray(analysis.names) ? analysis.names : [],
                numbers: Array.isArray(analysis.numbers) ? analysis.numbers : [],
                confidence: typeof analysis.confidence === 'number' ? analysis.confidence : 0.5,
            },
            model: raw.model ?? 'chandra',
            processingTimeMs: raw.processingTimeMs ?? 0,
            tokensUsed: raw.tokensUsed ?? 0,
        };
    }
    /**
     * Parse worker JSON output into ImageClassification.
     */
    parseClassificationResult(raw) {
        return {
            type: raw.type ?? 'other',
            hasText: raw.hasText ?? false,
            textDensity: raw.textDensity ?? 'unknown',
            complexity: raw.complexity ?? 'medium',
            confidence: typeof raw.confidence === 'number' ? raw.confidence : 0.5,
        };
    }
    /**
     * Destroy this client instance and release all resources.
     * Kills the daemon process to free GPU memory.
     */
    destroy() {
        this._cleanupDaemon();
    }
}
// Singleton management
let _client = null;
/**
 * Get or create ChandraClient singleton.
 */
export function getChandraClient() {
    if (!_client) {
        _client = new ChandraClient();
    }
    return _client;
}
/**
 * Reset singleton for testing.
 * Cleans up daemon process to prevent orphaned GPU-consuming workers.
 */
export function resetChandraClient() {
    if (_client) {
        _client.destroy();
    }
    _client = null;
}
//# sourceMappingURL=chandra.js.map