/**
 * Version check and auto-update utility
 * Checks npm registry for latest version and prompts/updates if needed
 */
import { execSync, spawn } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { getInstalledGlobalPackageVersion } from './npm-list-version.js';
import { getNpmCliJsPath, spawnNpmSync } from './npm-spawn.js';
import { sanitizePackageName } from './security/validation.js';
import { shutdownAutostartedGateway } from './local-gateway-pid.js';
import { isGlobalUpdateLockActive, releaseGlobalUpdateLock, tryAcquireGlobalUpdateLock, globalDeferredNpmLockPath, } from './deferred-npm-install-lock.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
 * Get current installed version from package.json
 */
function getCurrentVersion() {
    try {
        const installedVersion = getInstalledGlobalPackageVersion('4runr-os');
        if (installedVersion) {
            return installedVersion;
        }
    }
    catch {
        // Fall through to file-based check
    }
    // Fallback: Read from package.json
    try {
        const packagePath = path.join(__dirname, '..', 'package.json');
        const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
        return packageJson.version || '0.0.0';
    }
    catch {
        return '0.0.0';
    }
}
/**
 * Get latest version from npm registry
 */
async function getLatestVersion(packageName) {
    try {
        const { default: https } = await import('https');
        return new Promise((resolve, reject) => {
            https.get(`https://registry.npmjs.org/${packageName}/latest`, (res) => {
                let data = '';
                res.on('data', (chunk) => { data += chunk; });
                res.on('end', () => {
                    try {
                        const json = JSON.parse(data);
                        resolve(json.version || null);
                    }
                    catch {
                        resolve(null);
                    }
                });
            }).on('error', () => resolve(null));
        });
    }
    catch {
        // Fallback to npm view command
        try {
            // SECURITY: Validate package name to prevent command injection
            const { sanitizePackageName } = require('./security/validation.js');
            const validation = sanitizePackageName(packageName);
            if (!validation.valid || !validation.sanitized) {
                console.error(`Invalid package name: ${validation.error}`);
                return null;
            }
            // spawnNpmSync (node + npm-cli.js on Windows) instead of execSync
            const result = spawnNpmSync(['view', validation.sanitized, 'version']);
            if (result.status === 0 && result.stdout) {
                return result.stdout.trim() || null;
            }
            return null;
        }
        catch {
            return null;
        }
    }
}
/**
 * Compare version strings (simple semver comparison)
 */
function compareVersions(v1, v2) {
    const parts1 = v1.split('.').map(Number);
    const parts2 = v2.split('.').map(Number);
    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
        const p1 = parts1[i] || 0;
        const p2 = parts2[i] || 0;
        if (p1 < p2)
            return -1;
        if (p1 > p2)
            return 1;
    }
    return 0;
}
/**
 * Check if update is needed
 */
export async function checkForUpdates(packageName = '4runr-os') {
    const current = getCurrentVersion();
    const latest = await getLatestVersion(packageName) || current;
    const needsUpdate = compareVersions(current, latest) < 0;
    return { current, latest, needsUpdate };
}
/**
 * Prompt user to update (non-blocking, just shows message)
 */
export function promptUpdate(current, latest, packageName = '4runr-os') {
    process.stderr.write(`\n⚠️  Update available: ${current} → ${latest}\n`);
    process.stderr.write(`   Run: npm install -g ${packageName}@latest\n\n`);
}
/**
 * Clean up old npm cache entries and old global installs to prevent cache overflow
 */
function cleanupOldVersions(packageName) {
    try {
        // Clean npm cache for this package (keeps cache size down)
        // Only clean cache for this specific package, not entire cache
        try {
            // npm cache clean doesn't work per-package, so we skip this
            // to avoid clearing user's entire npm cache
            // Users can run `npm cache clean --force` manually if needed
        }
        catch {
            // Ignore cache clean errors - not critical
        }
        // Get npm global prefix to find old installs
        let globalPrefix;
        try {
            globalPrefix = execSync('npm config get prefix', {
                encoding: 'utf-8',
                stdio: 'pipe'
            }).trim();
        }
        catch {
            // Fallback to common locations
            const platform = process.platform;
            if (platform === 'win32') {
                globalPrefix = path.join(process.env.APPDATA || '', 'npm');
            }
            else {
                globalPrefix = '/usr/local/lib/node_modules';
            }
        }
        // Try to remove old versions from global node_modules (keep only current)
        // This is best-effort - npm handles most of this automatically
        try {
            const packageDir = path.join(globalPrefix, packageName);
            if (fs.existsSync(packageDir)) {
                // Check if there are old version directories (npm sometimes keeps them)
                const parentDir = path.dirname(packageDir);
                if (fs.existsSync(parentDir)) {
                    const entries = fs.readdirSync(parentDir, { withFileTypes: true });
                    // Look for old version directories like packageName@1.0.0, packageName@1.0.1, etc.
                    const oldVersions = entries
                        .filter(entry => entry.isDirectory() && entry.name.startsWith(`${packageName}@`))
                        .sort()
                        .slice(0, -2); // Keep last 2 versions, remove older ones
                    for (const oldVersion of oldVersions) {
                        try {
                            const oldPath = path.join(parentDir, oldVersion.name);
                            fs.rmSync(oldPath, { recursive: true, force: true });
                        }
                        catch {
                            // Ignore errors - might not have permissions
                        }
                    }
                }
            }
        }
        catch {
            // Ignore cleanup errors - not critical for functionality
        }
    }
    catch {
        // Silently fail - cleanup is best-effort
    }
}
/** Resolve path to globally installed package script (no shell spawn = no DEP0190) */
function getGlobalScriptPath(packageName) {
    const candidate = (p) => {
        const scriptPath = path.join(p, 'dist', 'index.js');
        return fs.existsSync(scriptPath) ? scriptPath : null;
    };
    const execOpts = { encoding: 'utf-8' };
    if (process.platform === 'win32')
        execOpts.shell = process.env.COMSPEC || 'cmd.exe';
    try {
        const root = execSync('npm root -g', execOpts).trim();
        const p = candidate(path.join(root, packageName));
        if (p)
            return p;
    }
    catch {
        // npm not in path or failed
    }
    try {
        const prefix = execSync('npm prefix -g', execOpts).trim();
        const p = candidate(path.join(prefix, 'node_modules', packageName));
        if (p)
            return p;
    }
    catch {
        // ignore
    }
    if (process.platform === 'win32' && process.env.APPDATA) {
        const p = candidate(path.join(process.env.APPDATA, 'npm', 'node_modules', packageName));
        if (p)
            return p;
    }
    return null;
}
/** Quote a path for a line inside a generated `.bat` file */
function quoteBatchPath(p) {
    return `"${p.replace(/"/g, '""')}"`;
}
/**
 * Windows: npm usually cannot replace `…\\node_modules\\4runr-os` while this Node process is still
 * running. After this process exits, run install + `4r` in a visible window so npm output and
 * errors (e.g. EBUSY) are not silently dropped.
 */
function spawnWindowsVisibleUpdater(safePkg) {
    const delaySec = 5;
    const npmCli = getNpmCliJsPath();
    const batPath = path.join(os.tmpdir(), `4runr-global-update-${process.pid}.bat`);
    /** Keeps the window readable: no dependency tree, no progress bar spam; errors still print. */
    const npmArgs = 'install -g ' + safePkg + '@latest --no-audit --no-fund --loglevel=error --progress=false';
    const lockPath = quoteBatchPath(globalDeferredNpmLockPath());
    const lines = [
        '@echo off',
        'setlocal EnableExtensions',
        'title 4Runr — install ^& launch',
        'cls',
        'echo.',
        'echo   4Runr — preparing the latest install, then starting the app in this window.',
        'echo   Keep this window open until the UI appears (npm may take several minutes).',
        'echo.',
        `timeout /t ${delaySec} /nobreak >nul`,
        'echo.',
        'echo   Installing (minimal npm output; errors will still show)...',
        'echo.',
    ];
    if (npmCli) {
        lines.push(`${quoteBatchPath(process.execPath)} ${quoteBatchPath(npmCli)} ${npmArgs}`);
    }
    else {
        lines.push(`call npm ${npmArgs}`);
    }
    lines.push('if errorlevel 1 goto :fail');
    lines.push('echo.');
    lines.push('echo   Done. Launching 4Runr...');
    lines.push('echo.');
    lines.push(`del /f /q ${lockPath} 2>nul`);
    lines.push('set NO_UPDATE_CHECK=1');
    // Launch 4r in THIS window (not a new one) - find the actual node path and run it directly
    const fourRunrScript = getGlobalScriptPath('4runr-os');
    if (fourRunrScript) {
        lines.push(`${quoteBatchPath(process.execPath)} ${quoteBatchPath(fourRunrScript)}`);
    }
    else {
        // Fallback: try to call 4r.cmd directly (but this may still open a new window)
        lines.push('4r');
    }
    lines.push('goto :eof');
    lines.push(':fail');
    lines.push(`del /f /q ${lockPath} 2>nul`);
    lines.push('echo.');
    lines.push('echo Install failed. Close every 4r window and stop Node, then try:');
    lines.push(`echo   npm install -g ${safePkg}@latest`);
    lines.push('pause');
    lines.push('exit /b 1');
    fs.writeFileSync(batPath, lines.join('\r\n'), 'utf-8');
    spawn('cmd.exe', ['/c', 'start', '4Runr update', 'cmd', '/c', batPath], {
        detached: true,
        stdio: 'ignore',
    }).unref();
}
/**
 * Same as the Windows update fallback: open a cmd window that runs `npm install -g` then `4r`.
 * Used when boot detects a broken global tree — npm cannot repair in-place while this process runs.
 */
export function spawnWindowsVisibleGlobalInstall(packageName = '4runr-os') {
    const nameCheck = sanitizePackageName(packageName);
    if (!nameCheck.valid || !nameCheck.sanitized)
        return;
    spawnWindowsVisibleUpdater(nameCheck.sanitized);
}
/**
 * Windows: relaunch 4r in a new console window (only after verified install — NO_UPDATE_CHECK set).
 */
function spawnWindowsRelaunchCli(packageName) {
    const scriptPath = getGlobalScriptPath(packageName);
    const batPath = path.join(os.tmpdir(), `4runr-relaunch-${process.pid}.bat`);
    const cwd = quoteBatchPath(process.cwd());
    const lockPath = quoteBatchPath(globalDeferredNpmLockPath());
    const lines = [
        '@echo off',
        'setlocal EnableExtensions',
        'title 4Runr OS',
        `del /f /q ${lockPath} 2>nul`,
        'set NO_UPDATE_CHECK=1',
        `cd /d ${cwd}`,
    ];
    if (scriptPath) {
        lines.push(`${quoteBatchPath(process.execPath)} ${quoteBatchPath(scriptPath)}`);
    }
    else {
        lines.push('call 4r');
    }
    lines.push('if errorlevel 1 (');
    lines.push('  echo.');
    lines.push('  echo   4Runr exited with an error. Press any key to close.');
    lines.push('  pause >nul');
    lines.push(')');
    fs.writeFileSync(batPath, lines.join('\r\n'), 'utf-8');
    spawn('cmd.exe', ['/c', 'start', '4Runr OS', 'cmd', '/k', batPath], {
        detached: true,
        stdio: 'ignore',
        windowsHide: true,
    }).unref();
}
/** Spawn freshly installed global CLI and exit this process (does not resolve). */
async function spawnUpdatedCliAfterInstall(packageName) {
    const { spawn: spawnChild } = await import('child_process');
    if (process.platform === 'win32') {
        spawnWindowsRelaunchCli(packageName);
        await new Promise((r) => setTimeout(r, 800));
        process.exit(0);
    }
    const args = [...process.argv.slice(2)];
    const spawnOpts = {
        stdio: 'inherit',
        cwd: process.cwd(),
        env: { ...process.env, NO_UPDATE_CHECK: '1' },
        detached: true,
    };
    const scriptPath = getGlobalScriptPath(packageName);
    let child;
    if (scriptPath) {
        child = spawnChild(process.execPath, [scriptPath, ...args], spawnOpts);
    }
    else {
        const updatedBinary = execSync('which 4r', { encoding: 'utf-8' }).split(/\r?\n/)[0]?.trim() || '4r';
        child = spawnChild(updatedBinary, args, spawnOpts);
    }
    child.on('error', (err) => {
        process.stderr.write(`\n⚠️  Update completed but launch failed: ${err.message}\n`);
        process.stderr.write(`   Run: 4r\n\n`);
        process.exit(1);
    });
    child.unref();
    process.exit(0);
}
/**
 * Auto-update (if user has permissions)
 * This function exits the process after updating, so it never returns normally
 */
export async function autoUpdate(packageName = '4runr-os') {
    try {
        if (isGlobalUpdateLockActive() && !tryAcquireGlobalUpdateLock()) {
            process.stderr.write('\n⏳ Update already in progress in another window — close extra 4Runr windows and wait.\n\n');
            process.exit(0);
        }
        if (!tryAcquireGlobalUpdateLock()) {
            process.stderr.write('\n⏳ Could not start update (another install may be running). Try again in a minute.\n\n');
            process.exit(0);
        }
        process.stderr.write(`\n🔄 New version on npm (installed copy is older): ${packageName}\n`);
        const nameCheck = sanitizePackageName(packageName);
        if (!nameCheck.valid || !nameCheck.sanitized) {
            releaseGlobalUpdateLock();
            throw new Error(nameCheck.error || 'Invalid package name');
        }
        const safePkg = nameCheck.sanitized;
        process.stderr.write(`Stopping local Gateway before update...\n`);
        shutdownAutostartedGateway();
        await new Promise((r) => setTimeout(r, 1500));
        cleanupOldVersions(packageName);
        const { safeNpmInstall } = await import('./security/package-integrity.js');
        // Windows: global tree is locked while this Node process runs — always defer to one updater window.
        if (process.platform === 'win32') {
            process.stderr.write(`\n📦 Opening one updater window — npm install, then 4r starts there automatically.\n` +
                `   Close this terminal if you like; do not open multiple 4r sessions during update.\n` +
                `   (Dev: NO_UPDATE_CHECK=1 skips update checks)\n\n`);
            spawnWindowsVisibleUpdater(safePkg);
            await new Promise((r) => setTimeout(r, 1200));
            process.exit(0);
        }
        process.stderr.write(`Installing update from npm registry...\n`);
        const result = safeNpmInstall(packageName, 'latest');
        if (!result.success) {
            releaseGlobalUpdateLock();
            throw new Error(`Failed to install update: ${result.error}`);
        }
        process.stderr.write(`✓ Update installed\n`);
        cleanupOldVersions(packageName);
        releaseGlobalUpdateLock();
        process.stderr.write(`✅ Updated successfully! Launching new version...\n\n`);
        await spawnUpdatedCliAfterInstall(packageName);
    }
    catch (error) {
        releaseGlobalUpdateLock();
        const errorMsg = error?.message || String(error);
        // If it's a permission error, show helpful message
        if (errorMsg.includes('EACCES') || errorMsg.includes('permission')) {
            process.stderr.write(`\n⚠️  Auto-update requires admin permissions.\n`);
            process.stderr.write(`   Please run: npm install -g ${packageName}@latest\n`);
            process.stderr.write(`   Or use sudo: sudo npm install -g ${packageName}@latest\n\n`);
        }
        else {
            process.stderr.write(`\n⚠️  Auto-update failed: ${errorMsg}\n`);
            if (errorMsg.includes('EBUSY') || errorMsg.includes('resource busy')) {
                process.stderr.write(`   Tip: exit all 4r and Gateway processes, then run: npm install -g ${packageName}@latest\n\n`);
            }
            else {
                process.stderr.write(`   Please run manually: npm install -g ${packageName}@latest\n\n`);
            }
        }
        process.exit(1); // Exit on error
    }
}
/**
 * Check and optionally update
 * If autoUpdateEnabled is true, will update and exit the process
 * User should run the command again to use the new version
 */
export async function checkAndUpdate(packageName = '4runr-os', autoUpdateEnabled = true) {
    // Skip check if disabled via env var (takes precedence over config)
    if (process.env.NO_UPDATE_CHECK === '1') {
        return;
    }
    if (isGlobalUpdateLockActive()) {
        process.stderr.write('\n⏳ Update in progress — wait for the updater window to finish.\n\n');
        process.exit(0);
    }
    try {
        const versionInfo = await checkForUpdates(packageName);
        if (versionInfo.needsUpdate) {
            if (autoUpdateEnabled) {
                // Auto-update will exit the process (never returns)
                await autoUpdate(packageName);
            }
            else {
                promptUpdate(versionInfo.current, versionInfo.latest, packageName);
            }
        }
    }
    catch (error) {
        // Silently fail - don't block startup if version check fails
    }
}
//# sourceMappingURL=version-check.js.map