/**
 * Local Cross-Encoder Reranker - TypeScript bridge to python/reranker_worker.py
 *
 * Spawns the Python cross-encoder worker to rerank search results locally
 * using ms-marco-MiniLM-L-12-v2. No cloud API calls.
 *
 * CRITICAL: NEVER use console.log() - stdout is reserved for JSON-RPC protocol.
 *
 * @module services/search/local-reranker
 */
import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** Max stderr accumulation: 10KB */
const MAX_STDERR_LENGTH = 10_240;
/** Timeout for reranker process: 30 seconds */
const RERANKER_TIMEOUT_MS = 30_000;
/** SIGKILL grace period after SIGTERM: 5 seconds */
const SIGKILL_GRACE_MS = 5_000;
/**
 * Rerank passages locally using the Python cross-encoder worker.
 *
 * @param query - The search query
 * @param passages - Passages to rerank with index, text, and original_score
 * @returns Reranked results sorted by relevance_score descending, or LocalRerankError with details
 */
export async function localRerank(query, passages) {
    const scriptPath = path.resolve(__dirname, '../../../python/reranker_worker.py');
    if (!fs.existsSync(scriptPath)) {
        const msg = `Python reranker script not found: ${scriptPath}`;
        console.error(`[local-reranker] ${msg}`);
        return { error: true, message: msg, error_type: 'SCRIPT_NOT_FOUND' };
    }
    const pythonPath = process.platform === 'win32' ? 'python' : 'python3';
    const inputPayload = JSON.stringify({ query, passages });
    return new Promise((resolve) => {
        const proc = spawn(pythonPath, [scriptPath], {
            timeout: RERANKER_TIMEOUT_MS,
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        let stdout = '';
        let stderr = '';
        let settled = false;
        let sigkillTimer = null;
        let timeoutTimer = null;
        const cleanup = () => {
            if (sigkillTimer) {
                clearTimeout(sigkillTimer);
                sigkillTimer = null;
            }
            if (timeoutTimer) {
                clearTimeout(timeoutTimer);
                timeoutTimer = null;
            }
        };
        proc.stdout.on('data', (data) => {
            stdout += data.toString();
        });
        proc.stderr.on('data', (data) => {
            if (stderr.length < MAX_STDERR_LENGTH) {
                stderr += data.toString();
            }
        });
        proc.on('error', (err) => {
            cleanup();
            if (settled)
                return;
            settled = true;
            console.error('[local-reranker] Process spawn error:', err.message);
            resolve({ error: true, message: `Reranker process spawn failed: ${err.message}`, error_type: 'SPAWN_ERROR' });
        });
        proc.on('close', (code, signal) => {
            cleanup();
            if (settled)
                return;
            settled = true;
            if (stderr.trim()) {
                console.error('[local-reranker] stderr:', stderr.trim().slice(0, 500));
            }
            // Parse stdout regardless of exit code — Python worker writes structured
            // error JSON to stdout even on non-zero exit. We must read it.
            let parsed;
            try {
                parsed = stdout.trim() ? JSON.parse(stdout) : null;
            }
            catch {
                // stdout wasn't valid JSON
                parsed = null;
            }
            if (code !== 0) {
                // Extract error details from Python worker's structured JSON output
                let errorDetail = `Process exited with code ${String(code)}, signal ${String(signal)}`;
                let errorType = 'PROCESS_EXIT_ERROR';
                if (parsed && typeof parsed === 'object' && 'error' in parsed) {
                    const errObj = parsed;
                    errorDetail = errObj.error;
                    errorType = errObj.error_type || errorType;
                }
                console.error(`[local-reranker] ${errorType}: ${errorDetail}`);
                resolve({ error: true, message: errorDetail, error_type: errorType });
                return;
            }
            // Check for error response (code === 0 but worker reported an error)
            if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'error' in parsed) {
                const errObj = parsed;
                console.error(`[local-reranker] Worker error: ${errObj.error}`);
                resolve({ error: true, message: errObj.error, error_type: errObj.error_type || 'WORKER_ERROR' });
                return;
            }
            if (!Array.isArray(parsed)) {
                const msg = `Expected array from reranker, got ${typeof parsed}. stdout: ${stdout.slice(0, 200)}`;
                console.error(`[local-reranker] ${msg}`);
                resolve({ error: true, message: msg, error_type: 'INVALID_OUTPUT' });
                return;
            }
            resolve(parsed);
        });
        // M-6: SIGKILL escalation after SIGTERM timeout
        proc.on('exit', () => {
            cleanup();
        });
        // Write input and close stdin
        proc.stdin.write(inputPayload);
        proc.stdin.end();
        // SIGKILL escalation: spawn's built-in timeout sends SIGTERM;
        // this timer escalates to SIGKILL if the process doesn't exit
        timeoutTimer = setTimeout(() => {
            if (!settled && !proc.killed) {
                console.error('[local-reranker] SIGKILL escalation after timeout grace period');
                proc.kill('SIGKILL');
            }
        }, RERANKER_TIMEOUT_MS + SIGKILL_GRACE_MS);
    });
}
//# sourceMappingURL=local-reranker.js.map