import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { createInterface } from 'node:readline';
import { homedir } from 'node:os';
import { join, resolve, extname, basename } from 'node:path';
import { randomUUID } from 'node:crypto';
const DOCKER_IMAGE = 'leapable/ocr-provenance-mcp';
const MODELS_IMAGE = 'leapable/ocr-provenance-models';
const CONTAINER_NAME = 'ocr-provenance-mcp';
const VOLUME_NAME = 'ocr-provenance-mcp-data';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Detect if running inside WSL (Windows Subsystem for Linux).
 */
function isWSLEnvironment() {
    try {
        return /microsoft|wsl/i.test(readFileSync('/proc/version', 'utf-8'));
    }
    catch {
        return false;
    }
}
/**
 * Run a command specified as an argument array.
 * Uses execFileSync (no shell) to prevent command injection.
 */
function execDocker(args, options) {
    try {
        return execFileSync('docker', args, {
            encoding: 'utf-8',
            stdio: options?.silent ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'inherit'],
        }).trim();
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        throw new Error(`Command failed: docker ${args.join(' ')}\n${message}`);
    }
}
function stderr(msg) {
    process.stderr.write(msg);
}
// ---------------------------------------------------------------------------
// Docker checks
// ---------------------------------------------------------------------------
export function checkDocker() {
    try {
        execFileSync('docker', ['--version'], { stdio: 'pipe' });
    }
    catch {
        throw new Error('Docker is not installed.\n\n' +
            'Install Docker:\n' +
            '  macOS/Windows: https://www.docker.com/products/docker-desktop\n' +
            '  Linux:         https://docs.docker.com/engine/install/\n\n' +
            'After installing, make sure the Docker daemon is running.');
    }
    try {
        execFileSync('docker', ['info'], { stdio: 'pipe' });
    }
    catch {
        throw new Error('Docker daemon is not running.\n\n' +
            'Start Docker Desktop or run:\n' +
            '  sudo systemctl start docker      # Linux with systemd\n' +
            '  open -a Docker                    # macOS');
    }
}
// ---------------------------------------------------------------------------
// Image management
// ---------------------------------------------------------------------------
export function pullImage(tag) {
    const fullImage = `${DOCKER_IMAGE}:${tag}`;
    stderr(`Pulling ${fullImage}...\n`);
    try {
        execFileSync('docker', ['pull', fullImage], { stdio: 'inherit' });
    }
    catch {
        throw new Error(`Failed to pull image ${fullImage}.\n\n` +
            'Possible causes:\n' +
            '  - No internet connection\n' +
            '  - Docker Hub rate limit exceeded (anonymous: 100 pulls/6h)\n' +
            '  - Image does not exist at that tag\n' +
            '  - Docker Hub repository is private (must be public)\n\n' +
            `Verify the image exists:\n` +
            `  docker pull ${fullImage}\n\n` +
            'If this persists, check https://hub.docker.com/r/leapable/ocr-provenance-mcp');
    }
}
export function isImageAvailable(tag) {
    try {
        execFileSync('docker', ['image', 'inspect', `${DOCKER_IMAGE}:${tag}`], { stdio: 'pipe' });
        return true;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// Models base image management
// ---------------------------------------------------------------------------
/**
 * Ensure the models base image is available locally.
 * The app image is FROM the models image, so Docker layer caching means
 * subsequent pulls of the app image only download new app layers (~500MB).
 * The models image layers (~24GB) are downloaded once and cached.
 */
export function pullModelsImage(modelsTag) {
    const fullImage = `${MODELS_IMAGE}:${modelsTag}`;
    stderr(`Ensuring models base image ${fullImage} is cached...\n`);
    try {
        // Check if already available locally
        execFileSync('docker', ['image', 'inspect', fullImage], { stdio: 'pipe' });
        stderr(`  Models image already cached locally.\n`);
    }
    catch {
        stderr(`  Pulling models image (${fullImage})...\n`);
        stderr(`  This contains all AI models (~24GB) and only needs to download once.\n`);
        try {
            execFileSync('docker', ['pull', fullImage], { stdio: 'inherit' });
        }
        catch {
            throw new Error(`Failed to pull models base image ${fullImage}.\n\n` +
                'The models image contains Python, PyTorch, and all AI models.\n' +
                'It must be available on Docker Hub before installing.\n\n' +
                'If this is a first install, this is a ~24GB download.\n' +
                'Subsequent updates only download code changes (~500MB).\n\n' +
                'Verify the image exists:\n' +
                `  docker pull ${fullImage}\n\n` +
                'If this persists, check https://hub.docker.com/r/leapable/ocr-provenance-models');
        }
    }
}
export function isModelsImageAvailable(modelsTag) {
    try {
        execFileSync('docker', ['image', 'inspect', `${MODELS_IMAGE}:${modelsTag}`], { stdio: 'pipe' });
        return true;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// Container lifecycle
// ---------------------------------------------------------------------------
export function isContainerRunning() {
    try {
        const result = execDocker(['inspect', '--format', '{{.State.Running}}', CONTAINER_NAME], { silent: true });
        return result === 'true';
    }
    catch {
        return false;
    }
}
function containerExists() {
    try {
        execDocker(['inspect', CONTAINER_NAME], { silent: true });
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Check if an existing container has stale mounts that require recreation.
 * Returns true if the container should be removed and recreated.
 * This catches containers created before WSL mount support was added.
 */
function hasStaleContainerMounts() {
    if (!isWSLEnvironment())
        return false;
    try {
        // Check if /mnt/c exists on the host (WSL with Windows drives)
        if (!existsSync('/mnt/c'))
            return false;
        // Inspect the container's environment for WSL mode
        const envJson = execDocker(['inspect', '--format', '{{json .Config.Env}}', CONTAINER_NAME], { silent: true });
        const envVars = JSON.parse(envJson);
        const hasWslMode = envVars.some((e) => e === 'OCR_PROVENANCE_WSL_MODE=1');
        if (!hasWslMode) {
            stderr('[docker] Existing container is missing WSL Windows drive mounts.\n');
            stderr('[docker] Recreating container to add /host-windows/ mounts...\n');
            return true;
        }
        return false;
    }
    catch {
        return false;
    }
}
export function startContainer(config) {
    const tag = config.imageTag ?? 'latest';
    const httpPort = config.httpPort ?? 3366;
    const dashboardPort = config.dashboardPort ?? 3367;
    // If container exists, check if it needs recreation for stale mounts
    if (containerExists()) {
        if (hasStaleContainerMounts()) {
            // Container is missing required mounts — stop and remove for recreation.
            // Data is safe in the named volume (ocr-provenance-mcp-data).
            stderr('[docker] Stopping and removing stale container (data volume preserved)...\n');
            try {
                execDocker(['stop', CONTAINER_NAME], { silent: true });
            }
            catch { /* may not be running */ }
            try {
                execDocker(['rm', CONTAINER_NAME], { silent: true });
            }
            catch { /* may already be gone */ }
            // Fall through to create a fresh container with correct mounts
        }
        else if (isContainerRunning()) {
            stderr('Container is already running.\n');
            return;
        }
        else {
            stderr('Starting existing container...\n');
            execDocker(['start', CONTAINER_NAME]);
            waitForHealth(httpPort, 60);
            return;
        }
    }
    // Build docker run arguments
    const args = [
        'run', '-d',
        '--name', CONTAINER_NAME,
        '--restart', 'unless-stopped',
        '-p', `${httpPort}:3366`,
        '-p', `${dashboardPort}:3367`,
        '-v', `${VOLUME_NAME}:/data`,
        '--entrypoint', '/usr/local/bin/docker-entrypoint.sh',
        // Security hardening: drop all capabilities and prevent privilege escalation.
        '--cap-drop=ALL',
        '--security-opt=no-new-privileges:true',
        '-e', 'MCP_TRANSPORT=http',
        '-e', 'MCP_HTTP_PORT=3366',
        '-e', 'OCR_PROVENANCE_DATABASES_PATH=/data',
        '-e', 'OCR_LICENSE_SERVER=http://127.0.0.1:3000',
        '-e', 'LICENSE_DB_PATH=/data/_license_server.db',
    ];
    // Mount user's home directory read-only so container can access host files for OCR
    const userHome = homedir();
    args.push('-v', `${userHome}:/host:ro`);
    args.push('-e', `OCR_PROVENANCE_HOST_HOME=${userHome}`);
    // On WSL, also mount Windows drives so users can ingest files from C:\Users\..., etc.
    // Docker Desktop for Windows can mount /mnt/c, /mnt/d, etc. from WSL2.
    const windowsMounts = [];
    if (isWSLEnvironment()) {
        for (const drive of ['c', 'd', 'e', 'f']) {
            const hostPath = `/mnt/${drive}`;
            if (existsSync(hostPath)) {
                const containerPath = `/host-windows/${drive}`;
                args.push('-v', `${hostPath}:${containerPath}:ro`);
                windowsMounts.push(containerPath);
            }
        }
        if (windowsMounts.length > 0) {
            args.push('-e', 'OCR_PROVENANCE_WSL_MODE=1');
            stderr(`[docker] WSL detected: mounted Windows drives ${windowsMounts.join(', ')}\n`);
        }
    }
    // Mount a writable export directory so users can export data out of the container.
    // On WSL, use the Windows user's Downloads folder so exports are accessible from Windows Explorer.
    let exportDir = join(userHome, 'ocr-provenance-exports');
    if (isWSLEnvironment()) {
        try {
            // Detect Windows home via cmd.exe (reliable across all setups)
            const winHome = execFileSync('cmd.exe', ['/c', 'echo', '%USERPROFILE%'], {
                encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
            }).trim().replace(/\\/g, '/').replace(/^([A-Za-z]):/, (_, d) => `/mnt/${d.toLowerCase()}`);
            if (winHome && existsSync(winHome)) {
                exportDir = join(winHome, 'ocr-provenance-exports');
                stderr(`[docker] Export directory: ${exportDir} (Windows-accessible)\n`);
            }
        }
        catch { /* cmd.exe may not be available — fall back to WSL home */ }
    }
    try {
        execFileSync('mkdir', ['-p', exportDir], { stdio: 'pipe' });
    }
    catch { /* may exist */ }
    args.push('-v', `${exportDir}:/export:rw`);
    const allowedDirs = ['/host', '/export', ...windowsMounts].join(',');
    args.push('-e', `OCR_PROVENANCE_ALLOWED_DIRS=${allowedDirs}`);
    // License key is NOT passed via -e (would be visible in docker inspect).
    // The container auto-provisions internally and persists to /data/.license_key.
    // Extra environment variables
    if (config.extraEnv) {
        for (const [k, v] of Object.entries(config.extraEnv)) {
            args.push('-e', `${k}=${v}`);
        }
    }
    // Pass machine ID from host persistence — primary defense against balance loss.
    // Same machine ID = same Cloudflare secrets = same license key = same balance.
    if (config.machineId) {
        args.push('-e', `MACHINE_ID=${config.machineId}`);
    }
    args.push(`${DOCKER_IMAGE}:${tag}`);
    // Always try with GPU first. If NVIDIA runtime is not available, Docker will
    // error — catch it and retry without --gpus. This is the correct cross-platform
    // pattern: works on Linux+NVIDIA, Windows+WSL2+NVIDIA, and gracefully degrades
    // on macOS or systems without NVIDIA runtime.
    // --runtime=nvidia ensures CUDA libs are visible inside the container.
    // NVIDIA_VISIBLE_DEVICES=all + NVIDIA_DRIVER_CAPABILITIES=compute,utility
    // ensure full GPU compute access for PyTorch/CUDA.
    const gpuArgs = [
        '--runtime=nvidia', '--gpus', 'all',
        '-e', 'NVIDIA_VISIBLE_DEVICES=all',
        '-e', 'NVIDIA_DRIVER_CAPABILITIES=compute,utility',
        ...args,
    ];
    const cpuArgs = args;
    stderr('Creating and starting container (trying GPU)...\n');
    try {
        execDocker(gpuArgs);
        stderr('[docker] GPU passthrough enabled.\n');
    }
    catch (gpuErr) {
        const gpuMsg = gpuErr instanceof Error ? gpuErr.message : String(gpuErr);
        // Check if the error is about missing NVIDIA runtime / GPU support
        if (gpuMsg.includes('could not select device driver') ||
            gpuMsg.includes('nvidia') ||
            gpuMsg.includes('GPU') ||
            gpuMsg.includes('OCI runtime') ||
            gpuMsg.includes('unknown or invalid runtime')) {
            stderr('[docker] GPU not available, running on CPU.\n');
            execDocker(cpuArgs);
        }
        else {
            // Some other Docker error — re-throw
            throw gpuErr;
        }
    }
    waitForHealth(httpPort, 60);
}
export function stopContainer() {
    if (!containerExists()) {
        stderr('No container to stop.\n');
        return;
    }
    stderr('Stopping container...\n');
    execDocker(['stop', '-t', '10', CONTAINER_NAME]);
    stderr('Container stopped.\n');
}
export function removeContainer() {
    if (!containerExists()) {
        stderr('No container to remove.\n');
        return;
    }
    if (isContainerRunning()) {
        stopContainer();
    }
    execDocker(['rm', CONTAINER_NAME]);
    stderr('Container removed.\n');
}
export function getContainerStatus() {
    if (!containerExists()) {
        return null;
    }
    try {
        const rawRunning = execDocker(['inspect', '--format', '{{.State.Running}}', CONTAINER_NAME], { silent: true });
        const rawHealth = execDocker(['inspect', '--format', '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}', CONTAINER_NAME], { silent: true });
        const rawStarted = execDocker(['inspect', '--format', '{{.State.StartedAt}}', CONTAINER_NAME], { silent: true });
        const running = rawRunning === 'true';
        let uptime = 'unknown';
        if (running && rawStarted) {
            const startMs = new Date(rawStarted).getTime();
            const nowMs = Date.now();
            const diffSec = Math.floor((nowMs - startMs) / 1000);
            if (diffSec < 60) {
                uptime = `${diffSec}s`;
            }
            else if (diffSec < 3600) {
                uptime = `${Math.floor(diffSec / 60)}m ${diffSec % 60}s`;
            }
            else {
                const h = Math.floor(diffSec / 3600);
                const m = Math.floor((diffSec % 3600) / 60);
                uptime = `${h}h ${m}m`;
            }
        }
        else if (!running) {
            uptime = 'stopped';
        }
        return { running, health: rawHealth, uptime };
    }
    catch {
        return null;
    }
}
export function getContainerLogs(lines) {
    if (!containerExists()) {
        throw new Error('No container found. Run `ocr-provenance-mcp start` first.');
    }
    try {
        // Capture both stdout and stderr from docker logs
        return execFileSync('docker', ['logs', '--tail', String(lines), CONTAINER_NAME], {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        }).trim();
    }
    catch (err) {
        // docker logs sends container stderr to host stderr; on "failure" the
        // output may still be in stderr of the child process
        if (err && typeof err === 'object' && 'stderr' in err && typeof err.stderr === 'string') {
            const stderrOutput = err.stderr.trim();
            if (stderrOutput)
                return stderrOutput;
        }
        const message = err instanceof Error ? err.message : String(err);
        throw new Error(`Failed to get container logs: ${message}`);
    }
}
// ---------------------------------------------------------------------------
// Health check
// ---------------------------------------------------------------------------
export function healthCheck(port) {
    const httpPort = port ?? 3366;
    try {
        const raw = execFileSync('curl', ['-sf', `http://localhost:${httpPort}/health`], {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        }).trim();
        const parsed = JSON.parse(raw);
        return {
            status: String(parsed['status'] ?? 'unknown'),
            tools: typeof parsed['tools'] === 'number' ? parsed['tools'] : 0,
        };
    }
    catch {
        return null;
    }
}
function getDockerHealthStatus() {
    try {
        return execDocker(['inspect', '--format', '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}', CONTAINER_NAME], { silent: true });
    }
    catch {
        return 'unknown';
    }
}
function waitForHealth(port, timeoutSeconds) {
    stderr(`Waiting for all services to be ready on port ${port}...`);
    const deadline = Date.now() + timeoutSeconds * 1000;
    let mcpReady = false;
    while (Date.now() < deadline) {
        // Check if container is still running
        if (!isContainerRunning()) {
            stderr('\n');
            throw new Error('Container exited unexpectedly during startup.\n' +
                'One or more services (license, MCP, dashboard) failed to start.\n' +
                'Check logs with: ocr-provenance-mcp logs 100');
        }
        // Phase 1: Wait for MCP server to respond
        if (!mcpReady) {
            const result = healthCheck(port);
            if (result && (result.status === 'ok' || result.status === 'operational')) {
                stderr(' MCP ready...');
                mcpReady = true;
                continue; // Immediately check Docker health
            }
            stderr('.');
            execFileSync('sleep', ['2'], { stdio: 'pipe' });
            continue;
        }
        // Phase 2: Wait for Docker healthcheck to confirm ALL services
        const dockerHealth = getDockerHealthStatus();
        if (dockerHealth === 'healthy') {
            stderr(' all services ready!\n');
            return;
        }
        if (dockerHealth === 'unhealthy') {
            stderr('\n');
            throw new Error('Container health check failed — one or more services are down.\n' +
                'All 3 services (license, MCP, dashboard) must be running.\n' +
                'Check logs with: ocr-provenance-mcp logs 100');
        }
        // dockerHealth is 'starting' or 'no-healthcheck' — keep waiting
        stderr('.');
        execFileSync('sleep', ['2'], { stdio: 'pipe' });
    }
    stderr('\n');
    throw new Error(`Health check timed out after ${timeoutSeconds} seconds.\n` +
        'The container may still be loading models.\n' +
        'All 3 services (license, MCP, dashboard) must be healthy.\n' +
        'Check status: ocr-provenance-mcp status\n' +
        'Check logs:   ocr-provenance-mcp logs 100');
}
// ---------------------------------------------------------------------------
// HTTP-to-stdio bridge (for MCP client integration)
// ---------------------------------------------------------------------------
/**
 * Bridge MCP stdio to the running container's HTTP /mcp endpoint.
 * Guarantees all 3 services (MCP, license, dashboard) are running.
 *
 * Flow: AI client stdin -> JSON-RPC -> HTTP POST /mcp -> response -> stdout
 *
 * If the container is not running, auto-starts it with the full entrypoint.
 * Container persists with --restart=unless-stopped -- no ephemeral containers.
 */
export function serveHttpBridge(config) {
    const tag = config.imageTag ?? 'latest';
    const httpPort = config.httpPort ?? 3366;
    // Verify image exists before attempting anything
    if (!isImageAvailable(tag)) {
        throw new Error(`Docker image ${DOCKER_IMAGE}:${tag} is not available locally.\n\n` +
            'Run this command first to download the image:\n' +
            '  npx -y ocr-provenance-mcp install\n\n' +
            'Or pull manually:\n' +
            `  docker pull ${DOCKER_IMAGE}:${tag}`);
    }
    // Ensure container is running with all 3 services (MCP, license, dashboard)
    if (!isContainerRunning()) {
        stderr('Container not running. Starting with full entrypoint...\n');
        startContainer(config);
    }
    const mcpUrl = `http://localhost:${httpPort}/mcp`;
    let sessionId;
    // Track the currently selected database so we can pass it to the upload
    // endpoint when intercepting ingest tool calls. The /api/upload REST endpoint
    // runs outside the MCP session context and cannot see which database was
    // selected via ocr_db_select — this bridges that gap.
    let selectedDatabase;
    const rl = createInterface({ input: process.stdin });
    rl.on('line', (line) => {
        const trimmed = line.trim();
        if (!trimmed)
            return;
        // Validate it looks like JSON before sending
        let parsed;
        try {
            parsed = JSON.parse(trimmed);
        }
        catch {
            stderr(`[bridge] Ignoring non-JSON input: ${trimmed.substring(0, 100)}\n`);
            return;
        }
        // Track database selection from ocr_db_select / ocr_db_create calls so
        // intercepted ingest calls can forward the database name to the upload
        // endpoint (which runs outside the MCP session context).
        trackDatabaseSelection(parsed);
        // Intercept ocr_ingest_files / ocr_ingest_directory tool calls.
        // These tools expect host file paths, but the container cannot read them
        // (bind mount permission issues). We upload files via the HTTP upload
        // endpoint instead, which is the ONLY reliable path into the container.
        if (isIngestToolCall(parsed)) {
            void handleIngestIntercept(parsed, httpPort, config);
            return;
        }
        // Fire-and-forget async -- we handle errors inside
        void bridgeRequest(mcpUrl, trimmed, parsed);
    });
    rl.on('close', () => {
        // stdin closed -- MCP client disconnected, exit cleanly
        process.exit(0);
    });
    /**
     * POST a JSON-RPC message to the container's /mcp endpoint.
     * Handles both application/json and text/event-stream responses.
     */
    async function bridgeRequest(url, body, parsed) {
        const headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json, text/event-stream',
        };
        if (config.licenseKey) {
            headers['X-License-Key'] = config.licenseKey;
        }
        if (sessionId) {
            headers['Mcp-Session-Id'] = sessionId;
        }
        try {
            const response = await fetch(url, {
                method: 'POST',
                headers,
                body,
            });
            // Capture session ID from response headers
            const respSessionId = response.headers.get('mcp-session-id');
            if (respSessionId) {
                sessionId = respSessionId;
            }
            if (!response.ok) {
                const errBody = await response.text();
                writeJsonRpcError(parsed, -32000, `HTTP ${response.status} from MCP server: ${errBody.substring(0, 500)}`);
                return;
            }
            const contentType = response.headers.get('content-type') ?? '';
            if (contentType.includes('text/event-stream')) {
                // SSE response -- read the stream line-by-line instead of buffering
                // the entire body. response.text() hangs on SSE because the server
                // keeps the connection open. Use the body reader to process data
                // events as they arrive.
                const reader = response.body?.getReader();
                if (!reader) {
                    writeJsonRpcError(parsed, -32000, 'SSE response has no readable body');
                    return;
                }
                const decoder = new TextDecoder();
                let buffer = '';
                let dataReceived = false;
                try {
                    while (true) {
                        const { done, value } = await reader.read();
                        if (done)
                            break;
                        buffer += decoder.decode(value, { stream: true });
                        // Process complete lines
                        const lines = buffer.split('\n');
                        buffer = lines.pop() ?? ''; // Keep incomplete last line
                        for (const sseLine of lines) {
                            if (sseLine.startsWith('data:')) {
                                const data = sseLine.substring(5).trim();
                                if (data && data !== '[DONE]') {
                                    process.stdout.write(data + '\n');
                                    dataReceived = true;
                                }
                            }
                        }
                        // For request-response patterns (initialize, tool calls),
                        // the server sends data then closes. For notifications, there
                        // may be no response. Break after first data to avoid hanging.
                        if (dataReceived) {
                            // Process any remaining buffer
                            if (buffer.startsWith('data:')) {
                                const data = buffer.substring(5).trim();
                                if (data && data !== '[DONE]') {
                                    process.stdout.write(data + '\n');
                                }
                            }
                            reader.cancel().catch(() => { }); // Release the stream
                            break;
                        }
                    }
                }
                catch (streamErr) {
                    // Stream read error — may happen on connection close, not fatal
                    stderr(`[bridge] SSE stream read error: ${streamErr instanceof Error ? streamErr.message : String(streamErr)}\n`);
                }
            }
            else {
                // JSON response -- pass through directly
                const jsonBody = await response.text();
                if (jsonBody.trim()) {
                    process.stdout.write(jsonBody.trim() + '\n');
                }
            }
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            stderr(`[bridge] Fetch error: ${message}\n`);
            writeJsonRpcError(parsed, -32000, `Bridge connection failed: ${message}`);
        }
    }
    /**
     * Write a JSON-RPC error response to stdout.
     * Extracts the request id from the parsed input if available.
     */
    function writeJsonRpcError(parsed, code, message) {
        let id = null;
        if (parsed && typeof parsed === 'object' && 'id' in parsed) {
            id = parsed.id;
        }
        const errorResponse = JSON.stringify({
            jsonrpc: '2.0',
            id: id ?? null,
            error: { code, message },
        });
        process.stdout.write(errorResponse + '\n');
    }
    /**
     * Write a JSON-RPC success response for a tool call to stdout.
     * Formats the result as an MCP tool result with a single text content block.
     */
    function writeJsonRpcToolResult(requestId, resultData) {
        const text = JSON.stringify(resultData, null, 2);
        const response = JSON.stringify({
            jsonrpc: '2.0',
            id: requestId ?? null,
            result: {
                content: [{ type: 'text', text }],
            },
        });
        process.stdout.write(response + '\n');
    }
    /**
     * Watch for ocr_db_select and ocr_db_create tool calls passing through the
     * bridge. When we see one, capture the database name so we can forward it
     * to the upload endpoint during intercepted ingest calls.
     *
     * The /api/upload REST endpoint runs outside the MCP session context and
     * cannot see which database was selected via ocr_db_select. This function
     * bridges that gap by tracking the selected database on the wrapper side.
     */
    function trackDatabaseSelection(parsed) {
        if (!parsed || typeof parsed !== 'object')
            return;
        const obj = parsed;
        if (obj.method !== 'tools/call')
            return;
        const params = obj.params;
        if (!params || typeof params !== 'object')
            return;
        const toolName = params.name;
        const args = params.arguments;
        if (!args)
            return;
        if (toolName === 'ocr_db_select') {
            const dbName = args.database_name;
            if (dbName) {
                selectedDatabase = dbName;
                stderr(`[bridge] Tracking selected database: ${dbName}\n`);
            }
        }
        else if (toolName === 'ocr_db_create') {
            // The server auto-selects newly created databases. Track the name here
            // so the wrapper stays in sync — otherwise a subsequent ingest call
            // would send the PREVIOUS database name to /api/upload.
            const dbName = args.name;
            if (dbName) {
                selectedDatabase = dbName;
                stderr(`[bridge] Tracking auto-selected database from create: ${dbName}\n`);
            }
        }
    }
    /**
     * Intercept ocr_ingest_files or ocr_ingest_directory tool calls.
     * Reads files from the HOST filesystem and uploads them via the container's
     * HTTP upload endpoint (POST /api/upload). This is the ONLY reliable way to
     * get host files into the container — bind mounts have permission issues.
     */
    async function handleIngestIntercept(parsed, port, cfg) {
        const msg = parsed;
        const requestId = msg.id;
        const toolName = msg.params.name;
        const args = msg.params.arguments ?? {};
        try {
            // Resolve the license key for auth
            const licenseKey = resolveUploadLicenseKey(cfg);
            // Use explicit database arg, fall back to the last database selected via
            // ocr_db_select. The /api/upload REST endpoint runs outside the MCP
            // session context, so we must explicitly pass the database name.
            const database = args.database ?? selectedDatabase;
            if (toolName === 'ocr_ingest_files') {
                const filePaths = args.file_paths;
                if (!filePaths || !Array.isArray(filePaths) || filePaths.length === 0) {
                    writeJsonRpcError(parsed, -32602, 'ocr_ingest_files requires a non-empty file_paths array');
                    return;
                }
                const result = await uploadFilesFromHost(filePaths, port, licenseKey, database);
                writeJsonRpcToolResult(requestId, result);
            }
            else if (toolName === 'ocr_ingest_directory') {
                const dirPath = args.directory_path;
                if (!dirPath || typeof dirPath !== 'string') {
                    writeJsonRpcError(parsed, -32602, 'ocr_ingest_directory requires a directory_path string');
                    return;
                }
                const recursive = args.recursive !== false; // default true
                const fileTypes = args.file_types;
                const includeHidden = args.include_hidden ?? false;
                const result = await uploadDirectoryFromHost(dirPath, port, licenseKey, recursive, fileTypes, includeHidden, database);
                writeJsonRpcToolResult(requestId, result);
            }
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            stderr(`[bridge] Ingest intercept error: ${message}\n`);
            writeJsonRpcError(parsed, -32000, `Host file upload failed: ${message}`);
        }
    }
}
// ---------------------------------------------------------------------------
// Ingest intercept: types and helpers
// ---------------------------------------------------------------------------
/** Supported file extensions for upload (hardcoded — wrapper is a separate package) */
const UPLOAD_SUPPORTED_TYPES = new Set([
    'pdf', 'docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls',
    'png', 'jpg', 'jpeg', 'tiff', 'tif', 'bmp', 'gif', 'webp',
    'txt', 'csv', 'md',
]);
/** Max files per upload batch (endpoint limit is 100, leave headroom) */
const UPLOAD_BATCH_SIZE = 80;
/**
 * Check if a parsed JSON-RPC message is a tools/call for an ingest tool.
 */
function isIngestToolCall(parsed) {
    if (!parsed || typeof parsed !== 'object')
        return false;
    const obj = parsed;
    if (obj.method !== 'tools/call')
        return false;
    const params = obj.params;
    if (!params || typeof params !== 'object')
        return false;
    const name = params.name;
    return name === 'ocr_ingest_files' || name === 'ocr_ingest_directory';
}
/**
 * Resolve the license key for upload endpoint auth.
 * Prefers config.licenseKey, falls back to reading from the container.
 */
function resolveUploadLicenseKey(config) {
    if (config.licenseKey)
        return config.licenseKey;
    try {
        const key = execFileSync('docker', ['exec', CONTAINER_NAME, 'cat', '/data/.license_key'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
        if (key)
            return key;
    }
    catch {
        // Container may not have a key yet — proceed without auth
    }
    return '';
}
/**
 * Resolve a user-provided file path on the HOST filesystem.
 * Handles Windows paths (C:\Users\...), WSL paths (/mnt/c/...), and Unix paths.
 */
function resolveHostPath(rawPath) {
    let normalized = rawPath;
    // Handle Windows-style paths: C:\Users\foo\file.pdf -> /mnt/c/Users/foo/file.pdf (on WSL)
    // or leave as-is on native Windows (though wrapper typically runs on WSL/Linux/macOS)
    const winDriveMatch = normalized.match(/^([A-Za-z]):[/\\]/);
    if (winDriveMatch) {
        const drive = winDriveMatch[1].toLowerCase();
        // Convert to WSL path format
        normalized = `/mnt/${drive}/${normalized.substring(3).replace(/\\/g, '/')}`;
    }
    else {
        // Replace any remaining backslashes with forward slashes
        normalized = normalized.replace(/\\/g, '/');
    }
    return resolve(normalized);
}
/**
 * Build a multipart/form-data body from files and optional form fields.
 * Uses only built-in Node.js APIs (no external dependencies).
 */
function buildMultipartBody(files, fields) {
    const boundary = '----FormBoundary' + randomUUID().replace(/-/g, '');
    const parts = [];
    // Add form fields first
    if (fields) {
        for (const [key, value] of Object.entries(fields)) {
            parts.push(Buffer.from(`--${boundary}\r\n` +
                `Content-Disposition: form-data; name="${key}"\r\n\r\n` +
                `${value}\r\n`));
        }
    }
    // Add file parts
    for (const file of files) {
        // Escape quotes and backslashes in filename to prevent Content-Disposition header corruption
        const safeFilename = file.filename.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
        parts.push(Buffer.from(`--${boundary}\r\n` +
            `Content-Disposition: form-data; name="${file.name}"; filename="${safeFilename}"\r\n` +
            `Content-Type: application/octet-stream\r\n\r\n`));
        parts.push(file.content);
        parts.push(Buffer.from('\r\n'));
    }
    parts.push(Buffer.from(`--${boundary}--\r\n`));
    return { boundary, body: Buffer.concat(parts) };
}
/**
 * POST files to the container's upload endpoint.
 * Returns the parsed upload result or throws on HTTP/network errors.
 */
async function postUploadBatch(fileParts, port, licenseKey, database) {
    const fields = {};
    if (database)
        fields.database = database;
    const { boundary, body } = buildMultipartBody(fileParts, fields);
    const headers = {
        'Content-Type': `multipart/form-data; boundary=${boundary}`,
    };
    if (licenseKey) {
        headers['X-License-Key'] = licenseKey;
    }
    const response = await fetch(`http://localhost:${port}/api/upload`, {
        method: 'POST',
        headers,
        body: new Blob([body]),
    });
    const responseBody = await response.text();
    try {
        return JSON.parse(responseBody);
    }
    catch {
        throw new Error(`Upload endpoint returned non-JSON: HTTP ${response.status} — ${responseBody.substring(0, 500)}`);
    }
}
/**
 * Upload specific files from the host filesystem via the HTTP upload endpoint.
 * Returns a result object matching the ocr_ingest_files response shape.
 */
async function uploadFilesFromHost(filePaths, port, licenseKey, database) {
    const items = [];
    const filesToUpload = [];
    // Phase 1: Read all files from host, collecting errors for unreadable ones
    for (const rawPath of filePaths) {
        const hostPath = resolveHostPath(rawPath);
        const fileName = basename(hostPath);
        if (!existsSync(hostPath)) {
            items.push({
                file_path: rawPath,
                file_name: fileName,
                document_id: '',
                status: 'error',
                error_message: `File not found on host: ${hostPath}`,
            });
            continue;
        }
        try {
            const st = statSync(hostPath);
            if (!st.isFile()) {
                items.push({
                    file_path: rawPath,
                    file_name: fileName,
                    document_id: '',
                    status: 'error',
                    error_message: `Not a regular file: ${hostPath}`,
                });
                continue;
            }
        }
        catch (err) {
            items.push({
                file_path: rawPath,
                file_name: fileName,
                document_id: '',
                status: 'error',
                error_message: `Cannot stat file: ${err instanceof Error ? err.message : String(err)}`,
            });
            continue;
        }
        const ext = extname(fileName).slice(1).toLowerCase();
        if (!UPLOAD_SUPPORTED_TYPES.has(ext)) {
            items.push({
                file_path: rawPath,
                file_name: fileName,
                document_id: '',
                status: 'error',
                error_message: `Unsupported file type: .${ext || '(none)'}. Supported: ${Array.from(UPLOAD_SUPPORTED_TYPES).join(', ')}`,
            });
            continue;
        }
        try {
            const content = readFileSync(hostPath);
            filesToUpload.push({ hostPath, fileName, content });
        }
        catch (err) {
            items.push({
                file_path: rawPath,
                file_name: fileName,
                document_id: '',
                status: 'error',
                error_message: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
            });
        }
    }
    // Phase 2: Upload in batches of UPLOAD_BATCH_SIZE
    for (let i = 0; i < filesToUpload.length; i += UPLOAD_BATCH_SIZE) {
        const batch = filesToUpload.slice(i, i + UPLOAD_BATCH_SIZE);
        const parts = batch.map((f) => ({
            name: 'files',
            content: f.content,
            filename: f.fileName,
        }));
        stderr(`[bridge] Uploading batch ${Math.floor(i / UPLOAD_BATCH_SIZE) + 1}: ${batch.length} file(s)...\n`);
        const result = await postUploadBatch(parts, port, licenseKey, database);
        if (!result.success) {
            // Entire batch failed — mark all as error
            for (const f of batch) {
                items.push({
                    file_path: f.hostPath,
                    file_name: f.fileName,
                    document_id: '',
                    status: 'error',
                    error_message: result.error?.message ?? 'Upload failed',
                });
            }
            continue;
        }
        const data = result.data;
        if (!data) {
            for (const f of batch) {
                items.push({
                    file_path: f.hostPath,
                    file_name: f.fileName,
                    document_id: '',
                    status: 'error',
                    error_message: 'Upload response missing data',
                });
            }
            continue;
        }
        // Map uploaded files back to items
        for (const uploaded of data.files) {
            const matchedFile = batch.find((f) => f.fileName === uploaded.original_name || f.fileName === uploaded.raw_original_name);
            items.push({
                file_path: matchedFile?.hostPath ?? uploaded.original_name,
                file_name: uploaded.original_name,
                document_id: uploaded.document_id,
                status: 'pending',
            });
        }
        // Map skipped (duplicate) files
        for (const skipped of data.skipped_files) {
            const matchedFile = batch.find((f) => f.fileName === skipped.original_name);
            items.push({
                file_path: matchedFile?.hostPath ?? skipped.original_name,
                file_name: skipped.original_name,
                document_id: skipped.existing_document_id,
                status: 'skipped',
                error_message: skipped.reason,
            });
        }
    }
    const filesIngested = items.filter((i) => i.status === 'pending').length;
    const filesSkipped = items.filter((i) => i.status === 'skipped').length;
    const filesErrored = items.filter((i) => i.status === 'error').length;
    return {
        success: true,
        data: {
            files_ingested: filesIngested,
            files_version_updated: 0,
            files_skipped: filesSkipped,
            files_errored: filesErrored,
            items,
            next_steps: [
                { tool: 'ocr_status', description: 'Check processing status — processing started automatically' },
            ],
        },
    };
}
/**
 * Recursively scan a host directory for supported files.
 */
function scanDirectory(dirPath, recursive, fileTypes, includeHidden) {
    const results = [];
    let names;
    try {
        names = readdirSync(dirPath, { encoding: 'utf-8' });
    }
    catch (err) {
        throw new Error(`Cannot read directory ${dirPath}: ${err instanceof Error ? err.message : String(err)}`);
    }
    for (const name of names) {
        // Skip hidden files/dirs unless requested
        if (!includeHidden && name.startsWith('.'))
            continue;
        const fullPath = join(dirPath, name);
        let st;
        try {
            st = statSync(fullPath);
        }
        catch {
            continue; // Skip inaccessible entries
        }
        if (st.isDirectory()) {
            if (recursive) {
                try {
                    results.push(...scanDirectory(fullPath, recursive, fileTypes, includeHidden));
                }
                catch {
                    // Skip unreadable subdirectories — report only top-level errors
                }
            }
            continue;
        }
        if (!st.isFile())
            continue;
        const ext = extname(name).slice(1).toLowerCase();
        if (fileTypes.has(ext)) {
            results.push(fullPath);
        }
    }
    return results;
}
/**
 * Upload a directory from the host filesystem via the HTTP upload endpoint.
 * Returns a result object matching the ocr_ingest_directory response shape.
 */
async function uploadDirectoryFromHost(rawDirPath, port, licenseKey, recursive, fileTypes, includeHidden, database) {
    const dirPath = resolveHostPath(rawDirPath);
    if (!existsSync(dirPath)) {
        throw new Error(`Directory not found on host: ${dirPath}`);
    }
    try {
        const st = statSync(dirPath);
        if (!st.isDirectory()) {
            throw new Error(`Path is not a directory: ${dirPath}`);
        }
    }
    catch (err) {
        if (err instanceof Error && err.message.startsWith('Path is not'))
            throw err;
        throw new Error(`Cannot stat directory: ${err instanceof Error ? err.message : String(err)}`);
    }
    // Determine which file types to scan for
    const typeSet = fileTypes && fileTypes.length > 0
        ? new Set(fileTypes.map((t) => t.toLowerCase().replace(/^\./, '')))
        : UPLOAD_SUPPORTED_TYPES;
    const files = scanDirectory(dirPath, recursive, typeSet, includeHidden ?? false);
    if (files.length === 0) {
        return {
            success: true,
            data: {
                directory_path: rawDirPath,
                files_found: 0,
                files_ingested: 0,
                files_version_updated: 0,
                files_skipped: 0,
                files_errored: 0,
                items: [],
                next_steps: [
                    { tool: 'ocr_ingest_directory', description: 'No supported files found. Try a different directory or file_types filter.' },
                ],
            },
        };
    }
    stderr(`[bridge] Found ${files.length} file(s) in ${dirPath}\n`);
    // Upload all found files using the same batch upload logic
    const result = await uploadFilesFromHost(files, port, licenseKey, database);
    // Augment result with directory-specific fields
    const data = result.data;
    data.directory_path = rawDirPath;
    data.files_found = files.length;
    return result;
}
// ---------------------------------------------------------------------------
// Exports for testing / advanced use
// ---------------------------------------------------------------------------
export { DOCKER_IMAGE, MODELS_IMAGE };
//# sourceMappingURL=docker.js.map