#!/usr/bin/env node
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
import { execFileSync, execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { homedir, platform } from 'node:os';
import { loadConfig, saveConfig, getConfigValue, setConfigValue, getConfigPath, } from './config.js';
import { DOCKER_IMAGE, MODELS_IMAGE, checkDocker, pullImage, pullModelsImage, isImageAvailable, isContainerRunning, startContainer, stopContainer, removeContainer, getContainerStatus, getContainerLogs, healthCheck, serveHttpBridge, } from './docker.js';
// ---------------------------------------------------------------------------
// Bare-metal constants
// ---------------------------------------------------------------------------
const BARE_METAL_DIR = join(homedir(), '.ocr-provenance-mcp');
const BARE_METAL_VENV = join(BARE_METAL_DIR, 'venv');
const BARE_METAL_MODELS = join(BARE_METAL_DIR, 'models');
// ---------------------------------------------------------------------------
// Version
// ---------------------------------------------------------------------------
function getVersion() {
    try {
        const __filename = fileURLToPath(import.meta.url);
        const __dirname = dirname(__filename);
        const pkgPath = join(__dirname, '..', 'package.json');
        const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
        return pkg.version ?? 'unknown';
    }
    catch {
        return 'unknown';
    }
}
// ---------------------------------------------------------------------------
// Output helpers (all user-facing output goes to stderr)
// ---------------------------------------------------------------------------
function log(msg) {
    process.stderr.write(msg + '\n');
}
function logError(msg) {
    process.stderr.write(`Error: ${msg}\n`);
}
// ---------------------------------------------------------------------------
// License key sync (container is the single source of truth)
// ---------------------------------------------------------------------------
/**
 * Sync the license key from the running container.
 *
 * The container's internal license server auto-provisions a key using a STABLE
 * machine ID persisted at /data/.machine_id. The wrapper MUST NOT independently
 * provision keys because:
 *   1. The wrapper's host-derived machine ID differs from the container's stable ID
 *   2. Different machine IDs produce different HMAC-derived keys
 *   3. Each independent provision creates an orphaned user in the license DB
 *
 * The container is always the authority. The wrapper syncs from it.
 * Retries up to 5 times (2s apart) to handle MCP server startup timing.
 * Throws on failure — the wrapper cannot operate with a stale or missing key.
 */
function syncLicenseKeyFromContainer(config) {
    const MAX_RETRIES = 5;
    const RETRY_DELAY_SEC = 2;
    for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
        try {
            // Read the persisted license key file written by the MCP server's initLicenseGate()
            const licenseKey = execFileSync('docker', [
                'exec', 'ocr-provenance-mcp',
                'cat', '/data/.license_key',
            ], {
                encoding: 'utf-8',
                stdio: ['pipe', 'pipe', 'pipe'],
                timeout: 10000,
            }).trim();
            if (licenseKey && licenseKey.startsWith('lk_')) {
                config.licenseKey = licenseKey;
                saveConfig(config);
                log(`License key synced from container: ${licenseKey.slice(0, 8)}...`);
                return;
            }
            // Key file exists but doesn't contain a valid key — retry
            if (attempt < MAX_RETRIES) {
                log(`[sync] Attempt ${attempt}/${MAX_RETRIES}: Key file exists but no valid key yet. Waiting ${RETRY_DELAY_SEC}s...`);
                execFileSync('sleep', [String(RETRY_DELAY_SEC)], { stdio: 'pipe' });
                continue;
            }
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            if (attempt < MAX_RETRIES) {
                log(`[sync] Attempt ${attempt}/${MAX_RETRIES}: ${msg.split('\n')[0]}. Waiting ${RETRY_DELAY_SEC}s...`);
                execFileSync('sleep', [String(RETRY_DELAY_SEC)], { stdio: 'pipe' });
                continue;
            }
            // Final attempt failed — throw with full context
            throw new Error(`Failed to sync license key from container after ${MAX_RETRIES} attempts.\n` +
                `Last error: ${msg.split('\n')[0]}\n\n` +
                'The MCP server started but did not write a valid license key to /data/.license_key.\n' +
                'Check container logs: ocr-provenance-mcp logs 100\n' +
                'Manual fix: ocr-provenance-mcp config set license_key=lk_YOUR_KEY');
        }
    }
    throw new Error(`License key file at /data/.license_key does not contain a valid key after ${MAX_RETRIES} attempts.\n` +
        'The container may have provisioning issues.\n' +
        'Check container logs: ocr-provenance-mcp logs 100');
}
// ---------------------------------------------------------------------------
// Machine ID sync (host persistence — primary defense against balance loss)
// ---------------------------------------------------------------------------
/**
 * Sync the machine ID from the running container.
 *
 * The container generates a stable machine ID at /data/.machine_id on first boot.
 * The wrapper persists this to wrapper.json on the HOST filesystem so it survives
 * Docker volume deletion. On subsequent container starts, the wrapper passes
 * MACHINE_ID env var back to the entrypoint, ensuring the same machine ID
 * (and therefore same Cloudflare secrets, same license key, same balance).
 *
 * This is the PRIMARY defense against balance loss on Docker restart/recreate.
 */
function syncMachineIdFromContainer(config) {
    try {
        const result = execFileSync('docker', [
            'exec', 'ocr-provenance-mcp',
            'cat', '/data/.machine_id',
        ], {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
            timeout: 10000,
        }).trim();
        if (result && result.length >= 8 && result !== config.machineId) {
            config.machineId = result;
            saveConfig(config);
            log(`Machine ID synced: ${result.slice(0, 8)}...`);
        }
    }
    catch {
        // Container may not have started yet — not an error
    }
}
// ---------------------------------------------------------------------------
// Account email sync (container writes email after Stripe payment)
// ---------------------------------------------------------------------------
/**
 * Sync the account email from the running container.
 *
 * The license server writes the user's email to /data/.account_email when
 * D1 sync returns an email (i.e., after the user pays via Stripe checkout).
 * The wrapper persists this to wrapper.json on the host so it can be passed
 * back on subsequent container starts, enabling balance recovery
 * via the /v1/account/recover-by-email endpoint if the machine ID changes.
 */
function syncAccountEmailFromContainer(config) {
    try {
        const result = execFileSync('docker', [
            'exec', 'ocr-provenance-mcp',
            'cat', '/data/.account_email',
        ], {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
            timeout: 10000,
        }).trim();
        if (result && result.includes('@') && result !== config.accountEmail) {
            config.accountEmail = result;
            saveConfig(config);
            log(`Account email synced: ${result}`);
        }
    }
    catch {
        // No email file yet — user hasn't paid via Stripe. This is normal.
    }
}
// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------
function cmdStart() {
    const startConfig = loadConfig();
    if (startConfig.extraEnv?.OCR_BARE_METAL === '1') {
        log('Bare-metal mode — no container to start.');
        log('The MCP server starts automatically when your AI client connects.');
        return;
    }
    checkDocker();
    const config = loadConfig();
    const tag = config.imageTag ?? 'latest';
    if (!isImageAvailable(tag)) {
        pullImage(tag);
    }
    startContainer(config);
    // ALWAYS sync license key from container — the container auto-provisions the key
    // and is the single source of truth. The wrapper.json key may be stale from a
    // previous install or different container instance.
    syncLicenseKeyFromContainer(config);
    syncMachineIdFromContainer(config);
    syncAccountEmailFromContainer(config);
    const httpPort = config.httpPort ?? 3366;
    const dashboardPort = config.dashboardPort ?? 3367;
    log('');
    log('OCR Provenance MCP is running.');
    log(`  MCP HTTP:   http://localhost:${httpPort}/mcp`);
    log(`  Health:     http://localhost:${httpPort}/health`);
    log(`  Dashboard:  http://localhost:${dashboardPort}`);
    log('');
    log('Use "ocr-provenance-mcp status" to check health.');
    log('Use "ocr-provenance-mcp logs" to view container logs.');
}
function cmdStop() {
    stopContainer();
}
function cmdRestart() {
    const wasRunning = isContainerRunning();
    if (wasRunning) {
        stopContainer();
    }
    // Remove old container so we can recreate with latest config
    removeContainer();
    cmdStart();
}
function cmdStatus() {
    const status = getContainerStatus();
    if (!status) {
        log('Container: not found');
        log('');
        log('Run "ocr-provenance-mcp start" to create and start the container.');
        return;
    }
    log(`Container: ${status.running ? 'running' : 'stopped'}`);
    log(`Health:    ${status.health}`);
    log(`Uptime:    ${status.uptime}`);
    if (status.running) {
        const config = loadConfig();
        const httpPort = config.httpPort ?? 3366;
        const dashboardPort = config.dashboardPort ?? 3367;
        const health = healthCheck(httpPort);
        if (health) {
            log(`Tools:     ${health.tools}`);
            log(`Status:    ${health.status}`);
        }
        log('');
        log(`MCP HTTP:  http://localhost:${httpPort}/mcp`);
        log(`Dashboard: http://localhost:${dashboardPort}`);
    }
}
function cmdLogs(lines) {
    const output = getContainerLogs(lines);
    // Logs go to stderr so they don't interfere with MCP stdio
    log(output);
}
function cmdUpdate() {
    checkDocker();
    const config = loadConfig();
    const tag = config.imageTag ?? 'latest';
    log(`Pulling latest app image (tag: ${tag})...`);
    log('  Models image cached locally — only downloading code changes.');
    pullImage(tag);
    if (isContainerRunning()) {
        log('Restarting container with updated image...');
        stopContainer();
        removeContainer();
        startContainer(config);
        syncLicenseKeyFromContainer(config);
        syncMachineIdFromContainer(config);
        syncAccountEmailFromContainer(config);
        const httpPort = config.httpPort ?? 3366;
        log('');
        log('Update complete. Container restarted with the latest image.');
        log(`  Health: http://localhost:${httpPort}/health`);
    }
    else {
        log('Image updated. Run "ocr-provenance-mcp start" to start the container.');
    }
}
function cmdConfigSet(pair) {
    const eqIdx = pair.indexOf('=');
    if (eqIdx === -1) {
        logError('Invalid format. Use: ocr-provenance-mcp config set KEY=VALUE');
        logError('Example: ocr-provenance-mcp config set license_key=lk_abc123');
        process.exit(1);
    }
    const key = pair.slice(0, eqIdx);
    const value = pair.slice(eqIdx + 1);
    if (!key) {
        logError('Key cannot be empty.');
        process.exit(1);
    }
    try {
        setConfigValue(key, value);
        log(`Set ${key} = ${value}`);
    }
    catch (err) {
        logError(err instanceof Error ? err.message : String(err));
        process.exit(1);
    }
}
function cmdConfigGet(key) {
    const value = getConfigValue(key);
    if (value === undefined) {
        log(`${key}: (not set)`);
    }
    else {
        log(`${key}: ${value}`);
    }
}
function cmdConfigShow() {
    const config = loadConfig();
    log(`Config file: ${getConfigPath()}`);
    log('');
    log(`license_key:    ${config.licenseKey ?? '(not set)'}`);
    log(`image_tag:      ${config.imageTag ?? 'latest'}`);
    log(`http_port:      ${config.httpPort ?? 3366}`);
    log(`dashboard_port: ${config.dashboardPort ?? 3367}`);
    log(`gpu:            auto-detected at runtime`);
    if (config.extraEnv && Object.keys(config.extraEnv).length > 0) {
        log(`extra_env:`);
        for (const [k, v] of Object.entries(config.extraEnv)) {
            log(`  ${k}=${v}`);
        }
    }
}
async function cmdServe() {
    // Check bare-metal mode. If installed with --mac/--bare, don't touch Docker.
    const serveConfig = loadConfig();
    if (serveConfig.extraEnv?.OCR_BARE_METAL === '1') {
        log('Bare-metal mode detected. The MCP server starts automatically when');
        log('your AI client connects (no manual serve needed).');
        log('');
        log('If your AI client is not connecting, verify registration:');
        log('  claude mcp list');
        log('');
        log('Or re-register:');
        log('  claude mcp add ocr-provenance-mcp -s user -- npx -y ocr-provenance-mcp');
        return;
    }
    checkDocker();
    let config = loadConfig();
    const tag = config.imageTag ?? 'latest';
    const fullImage = `${DOCKER_IMAGE}:${tag}`;
    // Fail fast if image is not available. Docker pull can take minutes and MCP clients
    // have a ~30s connection timeout, so pulling here would silently timeout.
    if (!isImageAvailable(tag)) {
        throw new Error(`Docker image ${fullImage} 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 ${fullImage}`);
    }
    // Ensure container is running so we can sync the key
    if (!isContainerRunning()) {
        startContainer(config);
    }
    // ALWAYS sync license key from container — it auto-provisions and is the
    // single source of truth. The key is needed for X-License-Key auth on the bridge.
    syncLicenseKeyFromContainer(config);
    syncMachineIdFromContainer(config);
    syncAccountEmailFromContainer(config);
    config = loadConfig();
    // Bridge stdin/stdout through HTTP to the running container's /mcp endpoint
    serveHttpBridge(config);
}
// ---------------------------------------------------------------------------
// Bare-metal helpers
// ---------------------------------------------------------------------------
function findPython() {
    for (const cmd of ['python3.12', 'python3.13', 'python3']) {
        try {
            const ver = execFileSync(cmd, ['--version'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
            const match = ver.match(/(\d+)\.(\d+)/);
            if (match && parseInt(match[1]) === 3 && parseInt(match[2]) >= 11) {
                return cmd;
            }
        }
        catch { /* not found */ }
    }
    throw new Error('Python 3.11+ required but not found.\n' +
        (platform() === 'darwin'
            ? '  Install: brew install python@3.12'
            : '  Install: sudo apt install python3.12 python3.12-venv'));
}
function shellRun(cmd, label) {
    try {
        execSync(cmd, { stdio: 'inherit', timeout: 600_000 });
    }
    catch (err) {
        throw new Error(`${label} failed: ${err instanceof Error ? err.message : String(err)}`);
    }
}
function hasCommand(cmd) {
    try {
        execFileSync('which', [cmd], { stdio: 'pipe', timeout: 5000 });
        return true;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// Bare-metal install (macOS / Linux without Docker)
// ---------------------------------------------------------------------------
async function cmdInstallBareMetal() {
    const isMac = platform() === 'darwin';
    log(`OCR Provenance MCP — Bare-Metal Install (${isMac ? 'macOS' : 'Linux'})`);
    log('');
    // Step 1: Find Python
    log('[1/6] Checking Python...');
    const pythonCmd = findPython();
    const pyVer = execFileSync(pythonCmd, ['--version'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
    log(`  ${pyVer} (${pythonCmd})`);
    // Step 2: Create venv
    log('[2/6] Creating Python virtual environment...');
    mkdirSync(BARE_METAL_DIR, { recursive: true });
    if (!existsSync(BARE_METAL_VENV)) {
        shellRun(`${pythonCmd} -m venv "${BARE_METAL_VENV}"`, 'venv creation');
        log(`  Created: ${BARE_METAL_VENV}`);
    }
    else {
        log('  Venv already exists.');
    }
    // Determine pip and python inside venv
    const venvPip = join(BARE_METAL_VENV, 'bin', 'pip');
    const venvPython = join(BARE_METAL_VENV, 'bin', 'python');
    // Step 3: Install Python dependencies
    log('[3/6] Installing Python dependencies...');
    // Determine torch install: MPS (macOS) or CUDA (Linux)
    if (isMac) {
        shellRun(`"${venvPip}" install --no-cache-dir torch torchvision`, 'PyTorch (MPS)');
    }
    else {
        // Try CUDA, fall back to CPU
        try {
            execSync('nvidia-smi', { stdio: 'pipe', timeout: 5000 });
            shellRun(`"${venvPip}" install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cu130`, 'PyTorch (CUDA)');
        }
        catch {
            log('  No NVIDIA GPU detected, installing CPU PyTorch...');
            shellRun(`"${venvPip}" install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu`, 'PyTorch (CPU)');
        }
    }
    // Pin torch constraints FIRST to prevent subsequent installs from replacing CUDA/MPS torch
    shellRun(`"${venvPip}" freeze | grep -iE "^(torch|torchvision)==" | sed "s/+.*$//" > "${BARE_METAL_VENV}/constraints.txt"`, 'Pin torch version');
    const constraintEnv = `PIP_CONSTRAINT="${BARE_METAL_VENV}/constraints.txt"`;
    // Find requirements.txt relative to the package
    const __filename_bm = fileURLToPath(import.meta.url);
    const __dirname_bm = dirname(__filename_bm);
    const localReqs = join(__dirname_bm, '..', 'python', 'requirements.txt');
    if (existsSync(localReqs)) {
        shellRun(`${constraintEnv} "${venvPip}" install --no-cache-dir -r "${localReqs}"`, 'Python deps from requirements.txt');
    }
    else {
        // All deps listed explicitly — no requirements.txt in npm package.
        // Pinned versions match python/requirements.txt to prevent conflicts.
        // marker-pdf, chandra-ocr, surya-ocr ALL use --no-deps because:
        //   - marker-pdf requires openai<2.0.0, chandra-ocr requires openai>=2.2.0 (conflict)
        //   - surya-ocr pulls transformers>=5.0.0, marker-pdf requires <5.0.0 (conflict)
        //   - All transitive deps must be listed explicitly below.
        shellRun(`${constraintEnv} "${venvPip}" install --no-cache-dir ` +
            // Core ML
            `sentence-transformers "transformers>=4.57.1,<5.0.0" einops numpy scipy ` +
            // Image processing
            `Pillow "PyMuPDF>=1.24.0" ` +
            // ML/clustering
            `scikit-learn ` +
            // PDF metadata extraction (used directly by ocr_worker_local.py)
            `"pypdf>=5.5.0" ` +
            // Shared transitive deps (used by marker-pdf, chandra-ocr, surya-ocr)
            `beautifulsoup4 "markdownify>=1.1.0,<2.0.0" six pydantic "pydantic-settings>=2.0.3" ` +
            `click filetype pypdfium2 python-dotenv ` +
            // Surya-ocr transitive deps (installed with --no-deps)
            `opencv-python-headless platformdirs ` +
            // Marker-pdf transitive deps (installed with --no-deps)
            `"google-genai>=1.0.0,<2.0.0" "anthropic>=0.46.0" ` +
            `pdftext ftfy markdown2 "rapidfuzz" "regex>=2024.4.28,<2025.0.0" tqdm accelerate ` +
            `openai qwen-vl-utils ` +
            // Document conversion
            `weasyprint mammoth python-pptx lxml "openpyxl>=3.1.0" "xlrd>=2.0.1" ` +
            // Compatibility (transitive deps of weasyprint/beautifulsoup4)
            `"soupsieve>=2.5" "urllib3>=2.3.0"`, 'Python dependencies');
    }
    // Install OCR/VLM packages with --no-deps (openai + transformers version conflicts)
    shellRun(`"${venvPip}" install --no-cache-dir --no-deps "surya-ocr"`, 'Surya OCR');
    shellRun(`"${venvPip}" install --no-cache-dir --no-deps "chandra-ocr>=0.2.0"`, 'Chandra OCR 2');
    shellRun(`"${venvPip}" install --no-cache-dir --no-deps "marker-pdf[full]>=1.10.0"`, 'Marker PDF');
    // Remove deprecated pynvml if present (replaced by nvidia-ml-py)
    try {
        execSync(`"${venvPip}" uninstall -y pynvml 2>/dev/null`, { stdio: 'pipe' });
    }
    catch { /* ok */ }
    log('  Python dependencies installed.');
    // Step 4: Download AI models
    log('[4/6] Downloading AI models (~14GB total, one-time)...');
    mkdirSync(BARE_METAL_MODELS, { recursive: true });
    // Ensure huggingface-hub is installed in venv for model downloads
    shellRun(`"${venvPip}" install --no-cache-dir huggingface-hub`, 'huggingface-hub');
    // huggingface_hub 1.7.x removed __main__.py — use the CLI module directly.
    // `python -m huggingface_hub download` does NOT work. Must use the full module path.
    const hfCli = `"${venvPython}" -m huggingface_hub.commands.huggingface_cli download`;
    // Download models (skip if already present)
    const chandraDir = join(homedir(), '.cache', 'huggingface', 'hub', 'models--datalab-to--chandra-ocr-2');
    if (!existsSync(chandraDir)) {
        log('  Downloading Chandra OCR 2 VLM (~10GB)...');
        shellRun(`${hfCli} datalab-to/chandra-ocr-2`, 'Chandra OCR 2 download');
    }
    else {
        log('  Chandra OCR 2: already cached.');
    }
    // Download nomic model to the path the embedding worker expects
    const nomicModelDir = join(BARE_METAL_DIR, 'models', 'nomic-embed-text-v1.5');
    const nomicCacheDir = join(homedir(), '.cache', 'huggingface', 'hub', 'models--nomic-ai--nomic-embed-text-v1.5');
    if (!existsSync(join(nomicModelDir, 'config.json')) && !existsSync(nomicCacheDir)) {
        log('  Downloading nomic-embed-text-v1.5 (~500MB)...');
        mkdirSync(join(BARE_METAL_DIR, 'models'), { recursive: true });
        shellRun(`${hfCli} nomic-ai/nomic-embed-text-v1.5 --local-dir "${nomicModelDir}"`, 'Nomic embedding download');
    }
    else if (!existsSync(join(nomicModelDir, 'config.json')) && existsSync(nomicCacheDir)) {
        // Model is in HF cache but not at the expected path — the embedding worker
        // now checks the HF cache directly, so just log it
        log('  nomic-embed-text-v1.5: found in HuggingFace cache.');
    }
    else {
        log('  nomic-embed-text-v1.5: already installed.');
    }
    log('  Marker/Surya models will download on first OCR run (~3.3GB).');
    // Step 5: Check LibreOffice (optional, for document viewer)
    log('[5/6] Checking optional dependencies...');
    if (hasCommand('libreoffice') || hasCommand('soffice')) {
        log('  LibreOffice: installed (document viewer will work).');
    }
    else {
        log('  LibreOffice: not found (document viewer Office→PDF conversion unavailable).');
        if (isMac) {
            log('  Optional: brew install --cask libreoffice');
        }
        else {
            log('  Optional: sudo apt install libreoffice-common');
        }
    }
    // Step 5b: Set up bundled dashboard
    // Dashboard is shipped as a tarball to avoid npm stripping node_modules/next/dist/.
    // npm's pack algorithm applies nested package.json `files` rules to node_modules
    // subdirectories, which strips Next.js's dist/ directory. A tarball bypasses this.
    const dashboardTarball = join(__dirname_bm, '..', '..', 'packages', 'wrapper', 'dashboard-bundle.tar.gz');
    const dashboardDest = join(BARE_METAL_DIR, 'dashboard');
    if (existsSync(dashboardTarball)) {
        log('  Dashboard: extracting from bundle...');
        if (existsSync(dashboardDest)) {
            shellRun(`rm -rf "${dashboardDest}"`, 'Remove old dashboard');
        }
        mkdirSync(BARE_METAL_DIR, { recursive: true });
        // Extract tarball — it contains dashboard-bundle/ as top-level dir.
        // Extract to temp, then rename to dashboard/.
        shellRun(`tar xzf "${dashboardTarball}" -C "${BARE_METAL_DIR}"`, 'Extract dashboard');
        // Rename dashboard-bundle/ to dashboard/
        const extractedDir = join(BARE_METAL_DIR, 'dashboard-bundle');
        if (existsSync(extractedDir)) {
            shellRun(`mv "${extractedDir}" "${dashboardDest}"`, 'Rename dashboard');
        }
        log(`  Dashboard installed: ${dashboardDest}`);
    }
    else {
        log('  Dashboard: tarball not found in package.');
        log('  Dashboard will be unavailable. OCR processing works without it.');
    }
    // Step 6: Register with AI clients
    log('[6/6] Registering with AI clients...');
    let registered = 0;
    // Save bare-metal mode to config
    const config = loadConfig();
    if (!config.extraEnv)
        config.extraEnv = {};
    config.extraEnv.OCR_BARE_METAL = '1';
    config.extraEnv.OCR_BARE_METAL_VENV = BARE_METAL_VENV;
    saveConfig(config);
    // Claude Code
    if (hasCLI('claude')) {
        log('  Found: Claude Code');
        try {
            execFileSync('claude', [
                'mcp', 'add', 'ocr-provenance-mcp', '-s', 'user',
                '--', 'npx', '-y', 'ocr-provenance-mcp',
            ], { stdio: ['pipe', 'pipe', 'pipe'] });
            log('    Registered via: claude mcp add');
            registered++;
        }
        catch {
            log('    Already registered or registration failed.');
        }
    }
    // Claude Desktop
    const claudeDesktopConfigBM = getClaudeDesktopConfigPath();
    if (claudeDesktopConfigBM) {
        log('  Found: Claude Desktop');
        if (addToJsonConfig(claudeDesktopConfigBM, 'mcpServers', 'ocr-provenance-mcp', {
            command: 'npx',
            args: ['-y', 'ocr-provenance-mcp'],
        })) {
            log(`    Registered in: ${claudeDesktopConfigBM}`);
            registered++;
        }
    }
    // Cursor
    const cursorConfigBM = getCursorConfigPath();
    if (cursorConfigBM) {
        log('  Found: Cursor');
        if (addToJsonConfig(cursorConfigBM, 'mcpServers', 'ocr-provenance-mcp', {
            command: 'npx',
            args: ['-y', 'ocr-provenance-mcp'],
        })) {
            log(`    Registered in: ${cursorConfigBM}`);
            registered++;
        }
    }
    // Windsurf
    const windsurfConfigBM = getWindsurfConfigPath();
    if (windsurfConfigBM) {
        log('  Found: Windsurf');
        if (addToJsonConfig(windsurfConfigBM, 'mcpServers', 'ocr-provenance-mcp', {
            command: 'npx',
            args: ['-y', 'ocr-provenance-mcp'],
        })) {
            log(`    Registered in: ${windsurfConfigBM}`);
            registered++;
        }
    }
    log('');
    log('Done! Bare-metal install complete.');
    log('');
    log(`  Python venv:  ${BARE_METAL_VENV}`);
    log(`  Models cache: ~/.cache/huggingface/hub/`);
    log(`  Config:       ${getConfigPath()}`);
    log(`  GPU:          ${isMac ? 'MPS (Apple Silicon)' : 'auto-detected at first run'}`);
    log('');
    if (registered > 0) {
        log(`Registered with ${registered} AI client(s). Restart your AI client to activate.`);
    }
    else {
        log('Register manually:');
        log('  claude mcp add ocr-provenance-mcp -s user -- npx -y ocr-provenance-mcp');
    }
    log('');
    log('The MCP server will start automatically when your AI client connects.');
    log('No Docker required.');
}
// ---------------------------------------------------------------------------
// Docker install
// ---------------------------------------------------------------------------
async function cmdInstall() {
    // Detect macOS and redirect to bare-metal install.
    // Docker on Mac cannot pass through MPS GPU — bare-metal is the correct path.
    // Docker images are amd64-only and would run under Rosetta (slow, no GPU).
    if (platform() === 'darwin') {
        log('OCR Provenance MCP — macOS Detected');
        log('');
        log('Docker images are built for Linux (amd64). On macOS, use bare-metal install');
        log('for native Apple Silicon GPU acceleration (MPS):');
        log('');
        log('  npx -y ocr-provenance-mcp install --mac');
        log('');
        log('This installs everything natively — no Docker required.');
        log('Run the command above to continue.');
        process.exit(1);
    }
    log('OCR Provenance MCP — Install');
    log('');
    // Step 1: Check Docker
    log('[1/5] Checking Docker...');
    checkDocker();
    log('  Docker OK.');
    // Step 2: Pull models base image (contains all AI models, ~14GB first time)
    log('[2/5] Pulling models base image...');
    pullModelsImage('v2');
    // Step 3: Pull app image (code + services, ~500MB)
    let config = loadConfig();
    const tag = config.imageTag ?? 'latest';
    log('[3/5] Pulling app image...');
    if (!isImageAvailable(tag)) {
        pullImage(tag);
    }
    else {
        log(`  Image ${tag} already available.`);
    }
    // Step 4: Start container + sync license key from it.
    // The container's internal license server auto-provisions a key using a STABLE
    // machine ID persisted at /data/.machine_id. The wrapper MUST NOT independently
    // provision keys (different machine ID = orphaned users in the license DB).
    log('[4/5] Starting container and syncing license key...');
    startContainer(config);
    syncLicenseKeyFromContainer(config);
    syncMachineIdFromContainer(config);
    syncAccountEmailFromContainer(config);
    // Reload config after sync
    config = loadConfig();
    // Step 5: Register with detected AI clients
    log('[5/5] Registering with AI clients...');
    let registered = 0;
    // Claude Code
    if (hasCLI('claude')) {
        log('  Found: Claude Code');
        try {
            execFileSync('claude', [
                'mcp', 'add', 'ocr-provenance-mcp', '-s', 'user',
                '--', 'npx', '-y', 'ocr-provenance-mcp',
            ], { stdio: ['pipe', 'pipe', 'pipe'] });
            log('    Registered via: claude mcp add');
            registered++;
        }
        catch {
            log('    Already registered or registration failed. Manual:');
            log('    claude mcp add ocr-provenance-mcp -s user -- npx -y ocr-provenance-mcp');
        }
    }
    // Claude Desktop
    const claudeDesktopConfig = getClaudeDesktopConfigPath();
    if (claudeDesktopConfig) {
        log('  Found: Claude Desktop');
        if (addToJsonConfig(claudeDesktopConfig, 'mcpServers', 'ocr-provenance-mcp', {
            command: 'npx',
            args: ['-y', 'ocr-provenance-mcp'],
        })) {
            log(`    Registered in: ${claudeDesktopConfig}`);
            registered++;
        }
        else {
            log('    Already registered.');
        }
    }
    // Cursor
    const cursorConfig = getCursorConfigPath();
    if (cursorConfig) {
        log('  Found: Cursor');
        if (addToJsonConfig(cursorConfig, 'mcpServers', 'ocr-provenance-mcp', {
            command: 'npx',
            args: ['-y', 'ocr-provenance-mcp'],
        })) {
            log(`    Registered in: ${cursorConfig}`);
            registered++;
        }
        else {
            log('    Already registered.');
        }
    }
    // Windsurf
    const windsurfConfig = getWindsurfConfigPath();
    if (windsurfConfig) {
        log('  Found: Windsurf');
        if (addToJsonConfig(windsurfConfig, 'mcpServers', 'ocr-provenance-mcp', {
            command: 'npx',
            args: ['-y', 'ocr-provenance-mcp'],
        })) {
            log(`    Registered in: ${windsurfConfig}`);
            registered++;
        }
        else {
            log('    Already registered.');
        }
    }
    log('');
    if (registered > 0) {
        log(`Done. Registered with ${registered} client(s).`);
        log('Restart your AI client to activate.');
    }
    else if (registered === 0 && !hasCLI('claude') && !claudeDesktopConfig && !cursorConfig && !windsurfConfig) {
        log('No AI clients detected on PATH. If you have Claude Code installed, register manually:');
        log('');
        log('  Claude Code:    claude mcp add ocr-provenance-mcp -s user -- npx -y ocr-provenance-mcp');
        log('  Claude Desktop: Add to claude_desktop_config.json');
        log('  Cursor:         Add to ~/.cursor/mcp.json');
        log('');
        log('MCP server config for any client:');
        log('  { "command": "npx", "args": ["-y", "ocr-provenance-mcp"] }');
    }
    else {
        log('All detected clients already registered.');
    }
    // Check for conflicting config in ~/.claude.json that could shadow our registration
    checkClaudeCodeConfigConflict();
}
// ---------------------------------------------------------------------------
// AI client detection helpers
// ---------------------------------------------------------------------------
function hasCLI(name) {
    // Try the standard PATH lookup first
    try {
        execFileSync(platform() === 'win32' ? 'where' : 'which', [name], { stdio: 'pipe' });
        return true;
    }
    catch {
        // On Windows, `where` only finds binaries on PATH. Some tools (e.g. Claude Code
        // installed via nvm4w) may not be on PATH in cmd.exe. Check common locations.
        if (platform() === 'win32' && name === 'claude') {
            const localAppData = process.env['LOCALAPPDATA'] ?? join(homedir(), 'AppData', 'Local');
            const appData = process.env['APPDATA'] ?? join(homedir(), 'AppData', 'Roaming');
            const candidates = [
                join(localAppData, 'Programs', 'claude-code', 'claude.exe'),
                join(appData, 'npm', 'claude.cmd'),
            ];
            return candidates.some((p) => existsSync(p));
        }
        return false;
    }
}
function getClaudeDesktopConfigPath() {
    const os = platform();
    let dir;
    if (os === 'darwin') {
        dir = join(homedir(), 'Library', 'Application Support', 'Claude');
    }
    else if (os === 'win32') {
        dir = join(process.env['APPDATA'] ?? join(homedir(), 'AppData', 'Roaming'), 'Claude');
    }
    else {
        dir = join(homedir(), '.config', 'Claude');
    }
    const configPath = join(dir, 'claude_desktop_config.json');
    // Return the path if the directory exists (config may not exist yet — we'll create it)
    if (existsSync(dir) || existsSync(configPath)) {
        return configPath;
    }
    return null;
}
function getCursorConfigPath() {
    const cursorDir = join(homedir(), '.cursor');
    if (existsSync(cursorDir)) {
        return join(cursorDir, 'mcp.json');
    }
    return null;
}
function getWindsurfConfigPath() {
    const os = platform();
    let dir;
    if (os === 'darwin') {
        dir = join(homedir(), 'Library', 'Application Support', 'Windsurf');
    }
    else if (os === 'win32') {
        dir = join(process.env['APPDATA'] ?? join(homedir(), 'AppData', 'Roaming'), 'Windsurf');
    }
    else {
        dir = join(homedir(), '.config', 'Windsurf');
    }
    const configPath = join(dir, 'mcp_config.json');
    if (existsSync(dir) || existsSync(configPath)) {
        return configPath;
    }
    return null;
}
/**
 * Add an MCP server entry to a JSON config file.
 * Creates the file if it doesn't exist. Returns true if entry was added, false if already present.
 */
function addToJsonConfig(configPath, section, serverName, serverConfig) {
    let config = {};
    if (existsSync(configPath)) {
        try {
            config = JSON.parse(readFileSync(configPath, 'utf-8'));
        }
        catch {
            // Corrupted file — back up and start fresh
            config = {};
        }
    }
    if (!config[section]) {
        config[section] = {};
    }
    if (config[section][serverName]) {
        return false; // Already registered
    }
    config[section][serverName] = serverConfig;
    // Ensure parent directory exists
    const dir = dirname(configPath);
    if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true });
    }
    writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
    return true;
}
/**
 * Check for a conflicting ocr-provenance-mcp entry in ~/.claude.json.
 * Claude Code reads from both ~/.claude.json and ~/.claude/settings.json.
 * If ~/.claude.json has an "ocr-provenance-mcp" MCP entry, it takes precedence
 * over the one we just wrote to settings.json, silently ignoring the new config.
 */
function checkClaudeCodeConfigConflict() {
    const claudeJsonPath = join(homedir(), '.claude.json');
    if (!existsSync(claudeJsonPath)) {
        return;
    }
    try {
        const raw = readFileSync(claudeJsonPath, 'utf-8');
        const parsed = JSON.parse(raw);
        const mcpServers = parsed['mcpServers'];
        if (mcpServers && mcpServers['ocr-provenance-mcp']) {
            log('');
            log('WARNING: Conflicting MCP config detected.');
            log(`  File: ${claudeJsonPath}`);
            log('  Found "ocr-provenance-mcp" under mcpServers in ~/.claude.json.');
            log('  This entry takes precedence over the one just registered');
            log('  in ~/.claude/settings.json, and may cause the new config to be ignored.');
            log('');
            log('  To fix: Remove the "ocr-provenance-mcp" entry from ~/.claude.json');
            log('  or delete the mcpServers section if it only contains that entry.');
        }
    }
    catch {
        // File is unreadable or not valid JSON — nothing to warn about
    }
}
function cmdUninstall() {
    log('Uninstalling OCR Provenance MCP container...');
    removeContainer();
    log('');
    log('Container removed. Data volume preserved for safety.');
    log('');
    log('To also remove data:');
    log('  docker volume rm ocr-provenance-mcp-data');
    log('');
    log('To also remove the Docker images:');
    log(`  docker rmi ${DOCKER_IMAGE}:latest`);
    log(`  docker rmi ${MODELS_IMAGE}:v2   # models (~14GB)`);
}
function showHelp() {
    const version = getVersion();
    log(`ocr-provenance-mcp v${version} - Document intelligence via MCP`);
    log('');
    log('Quick start:');
    log('  npx -y ocr-provenance-mcp install         Docker install (Linux/WSL, NVIDIA GPU)');
    log('  npx -y ocr-provenance-mcp install --mac    Bare-metal install (macOS Apple Silicon)');
    log('  npx -y ocr-provenance-mcp install --bare   Bare-metal install (Linux, no Docker)');
    log('');
    log('Usage: ocr-provenance-mcp [command]');
    log('');
    log('  (no command)       Run MCP stdio bridge (default — used by AI clients)');
    log('  install            Docker install: pull images, register with AI clients');
    log('  install --mac      Bare-metal: Python venv + models, no Docker (macOS)');
    log('  install --bare     Bare-metal: Python venv + models, no Docker (Linux)');
    log('  start              Start container in HTTP mode (dashboard + all services)');
    log('  stop               Stop the container');
    log('  restart            Restart the container');
    log('  status             Show container health and endpoints');
    log('  logs [N]           Show last N lines of logs (default: 50)');
    log('  update             Pull latest image and restart');
    log('  config set K=V     Set a config value');
    log('  config get KEY     Get a config value');
    log('  config show        Show all config');
    log('  serve              Explicit stdio mode (same as no command)');
    log('  uninstall          Remove container (keeps data volume)');
    log('');
    log('Config keys: license_key, image_tag, http_port, dashboard_port');
    log('');
    log('One-liner for any MCP client:');
    log('  { "command": "npx", "args": ["-y", "ocr-provenance-mcp"] }');
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
export async function runCli(cliArgs) {
    const args = cliArgs ?? process.argv.slice(2);
    const command = args[0];
    // No args = serve (stdio mode). This is the zero-config path.
    // AI clients call: npx -y ocr-provenance-mcp
    if (!command) {
        await cmdServe();
        return;
    }
    if (command === '--help' || command === '-h') {
        showHelp();
        process.exit(0);
    }
    if (command === '--version' || command === '-v') {
        log(getVersion());
        process.exit(0);
    }
    // Check for --mac or --bare flag on install command
    const hasBareMetal = args.includes('--mac') || args.includes('--bare');
    switch (command) {
        case 'install':
            if (hasBareMetal) {
                await cmdInstallBareMetal();
            }
            else {
                await cmdInstall();
            }
            break;
        case 'start':
            cmdStart();
            break;
        case 'stop':
            cmdStop();
            break;
        case 'restart':
            cmdRestart();
            break;
        case 'status':
            cmdStatus();
            break;
        case 'logs': {
            const lines = args[1] ? parseInt(args[1], 10) : 50;
            if (isNaN(lines) || lines < 1) {
                logError('Invalid line count. Usage: ocr-provenance-mcp logs [N]');
                process.exit(1);
            }
            cmdLogs(lines);
            break;
        }
        case 'update':
            cmdUpdate();
            break;
        case 'config': {
            const subCmd = args[1];
            if (subCmd === 'set') {
                if (!args[2]) {
                    logError('Usage: ocr-provenance-mcp config set KEY=VALUE');
                    process.exit(1);
                }
                cmdConfigSet(args[2]);
            }
            else if (subCmd === 'get') {
                if (!args[2]) {
                    logError('Usage: ocr-provenance-mcp config get KEY');
                    process.exit(1);
                }
                cmdConfigGet(args[2]);
            }
            else if (subCmd === 'show') {
                cmdConfigShow();
            }
            else {
                logError(`Unknown config subcommand: ${subCmd ?? '(none)'}`);
                log('Usage: ocr-provenance-mcp config <set|get|show>');
                process.exit(1);
            }
            break;
        }
        case 'serve':
            await cmdServe();
            break;
        case 'setup':
            await cmdInstall();
            break;
        case 'uninstall':
            cmdUninstall();
            break;
        default:
            logError(`Unknown command: ${command}`);
            log('');
            showHelp();
            process.exit(1);
    }
}
runCli().catch((err) => {
    logError(err instanceof Error ? err.message : String(err));
    process.exit(1);
});
//# sourceMappingURL=cli.js.map